Compare commits
491 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf67e92d44 | |||
| c17eeb6b44 | |||
| 37eea2d353 | |||
| 8c9a092561 | |||
| e421fe9860 | |||
| e13d4c9c13 | |||
| 640f53fe0a | |||
| 1bca1b8bde | |||
| c902c93082 | |||
| f1ca1794a1 | |||
| ad5d115abe | |||
| 56079527ef | |||
| c77b9f359f | |||
| 7f504ec5fc | |||
| b1bcd67f5a | |||
| 2a26574808 | |||
| 1d427d0581 | |||
| 321868963d | |||
| 1529b67e41 | |||
| 190e4b55eb | |||
| 9e122764e7 | |||
| 327b9bd59c | |||
| 301c217303 | |||
| 9883473376 | |||
| 6631e6eedc | |||
| 933b6b67f5 | |||
| 56e0e5d6ad | |||
| 369bd17c8b | |||
| 9681f5b360 | |||
| b107894976 | |||
| 796c933fb8 | |||
| d43daaee81 | |||
| b6cdffa509 | |||
| 7b1562c050 | |||
| 20583d5334 | |||
| 2d03d7c373 | |||
| dd15d33bce | |||
| c5e8f38e1e | |||
| db45ec7434 | |||
| 4f4bceec94 | |||
| 7a16233584 | |||
| fff12e3d78 | |||
| da750ed838 | |||
| e49512896f | |||
| 0075243ed5 | |||
| 29e47dd7c1 | |||
| 105a74caca | |||
| 55b9acca78 | |||
| 0d130b8957 | |||
| 0aa5d3daeb | |||
| 4b622e6cfa | |||
| 82aeb3292a | |||
| 335c03d0b8 | |||
| 4681ff88d0 | |||
| 33fd27f113 | |||
| 527fd7d472 | |||
| 3a69e5e819 | |||
| 7e611fa99c | |||
| 71d346207d | |||
| 56d27bc1b4 | |||
| e1f8e96e28 | |||
| ab97f03cb5 | |||
| a2e38270e4 | |||
| 8f981b6052 | |||
| 939e91f9ed | |||
| 22c3d26604 | |||
| 7aaf48cb0c | |||
| afd7815420 | |||
| e5fe68cbf6 | |||
| 3b0fff3b3d | |||
| ec7015be88 | |||
| 19fafd8c10 | |||
| e47a89b274 | |||
| 66650f5944 | |||
| 0529076ed7 | |||
| 7f854432ae | |||
| 15a2e6feeb | |||
| 4ed68cf673 | |||
| 8337da183c | |||
| 6dfa9e1146 | |||
| 282bccaca5 | |||
| 62d307321a | |||
| f7afc0334e | |||
| 28e8e539f6 | |||
| 7cc19b50fc | |||
| 97c340b8a4 | |||
| 7b1d4a6787 | |||
| 0714d119d7 | |||
| 700622e521 | |||
| 3682e76dee | |||
| cd0e537e3e | |||
| 0849dbd1af | |||
| 4ab4a35eba | |||
| e5d9372708 | |||
| 8edc2fb46f | |||
| e520c0d1f5 | |||
| 506f9f6fb9 | |||
| 3cb8f54307 | |||
| ee4d9fff16 | |||
| 27be813011 | |||
| 861b72ef04 | |||
| fd83280b70 | |||
| 169d9d18b0 | |||
| 245535ee04 | |||
| 5bc9158724 | |||
| 6a4bc777a2 | |||
| 34cbb18ecd | |||
| e2d5a8c0bb | |||
| 94cd806675 | |||
| b6af7788e1 | |||
| c4bb9f49ff | |||
| 8e5695f06d | |||
| 395c28f5fa | |||
| 3e5183606c | |||
| 6a36bbd1d1 | |||
| 4b39d37cae | |||
| 25c9b779e4 | |||
| fcc3b81745 | |||
| 6f677b4fae | |||
| 78f202603c | |||
| b8c5363a15 | |||
| b8b3c487d4 | |||
| ec48fccb30 | |||
| 3f61019ca1 | |||
| 16513b4a6e | |||
| 9b705e4450 | |||
| e1c2135850 | |||
| 9fe80c25eb | |||
| 13b11a39a9 | |||
| 8bf571bf48 | |||
| 613b544bf0 | |||
| 916603d2d4 | |||
| e30eecba2c | |||
| 3a94be0212 | |||
| 6295edcdb7 | |||
| 335b4937ed | |||
| 06da0469c4 | |||
| 1ad893ded4 | |||
| 636f5fb933 | |||
| c45e28ab53 | |||
| c56c04a82b | |||
| d431d37454 | |||
| 1694dd146e | |||
| 4a6a0aa142 | |||
| c788160532 | |||
| cc66159f04 | |||
| c58a70ac8f | |||
| 1855aaea99 | |||
| 3901b5da44 | |||
| 5dc59b591d | |||
| 96a5710932 | |||
| d36d32d07b | |||
| 727b3b9f53 | |||
| c85563da50 | |||
| a771c563ba | |||
| 3cc800f93a | |||
| b449feb3e1 | |||
| b07a565e34 | |||
| 787eebcf1e | |||
| 604b8ff17c | |||
| 6e93ddf2f1 | |||
| b6e4be72f0 | |||
| 75aa8e6621 | |||
| 5b7417bf64 | |||
| db744f500b | |||
| a56cf35d8c | |||
| d1e6843f3e | |||
| d18868873e | |||
| 827014fa4b | |||
| 944b33983c | |||
| 2641185af2 | |||
| 64aac239f0 | |||
| d6823b128c | |||
| 508f32c08a | |||
| 8ed6ed4d2b | |||
| 1abb0bdae8 | |||
| 5ef6215546 | |||
| 95fb9c4365 | |||
| fa0a5107c2 | |||
| dc3c329431 | |||
| 2a9f2b4515 | |||
| 793049388b | |||
| 382b63954c | |||
| 87ccba7f9d | |||
| e21c96c0ef | |||
| 4de0b2f44e | |||
| b588a87d4a | |||
| 44ed1f0919 | |||
| 16d0df796c | |||
| 9fd5d2ad9c | |||
| 28ad004b01 | |||
| ef4a492cb1 | |||
| 6d9e7694b1 | |||
| 0c13c63bb6 | |||
| 907eb869bc | |||
| c1402eee8e | |||
| 84f7ca855a | |||
| 2dcce03352 | |||
| 96a22ec3c1 | |||
| 4b29bccc7c | |||
| 40e079a247 | |||
| 81f0265095 | |||
| 92cc647cf6 | |||
| 048d437b0b | |||
| ec9a6bca14 | |||
| bd5952b943 | |||
| 3f0d54c752 | |||
| dab4595a4e | |||
| 6d9ca82b19 | |||
| 373a03e819 | |||
| d97b0259fa | |||
| 2267ca1949 | |||
| 29be53e70d | |||
| 851fe4a49f | |||
| 30f499cf2e | |||
| 591a641d8d | |||
| 5b314ffd46 | |||
| 0b078c9f99 | |||
| 0d5584ecbb | |||
| 5e090646ba | |||
| c4e910dd3d | |||
| 5a2394af7c | |||
| 48e32269f4 | |||
| dd9d90d21e | |||
| 0544c687b9 | |||
| e810aae212 | |||
| 9c6a26de9f | |||
| e6f2bb9f89 | |||
| f908bd4a64 | |||
| 7395b03b1f | |||
| 63b4fc6f65 | |||
| f392fe7702 | |||
| 2daed747cd | |||
| 9e4bab7494 | |||
| 9274c0701b | |||
| 0bc773fd00 | |||
| c6d2408517 | |||
| 033f83a55a | |||
| 51841d627c | |||
| 50924f0b3d | |||
| 4aae1da841 | |||
| 1a2554548a | |||
| 40262c30cb | |||
| 761e7fdd2d | |||
| cd8a124b25 | |||
| 148428a564 | |||
| 14da671bf9 | |||
| e8f0f82db0 | |||
| b8278404a0 | |||
| 45671b0b8b | |||
| 321525ead5 | |||
| 1d24e20d22 | |||
| 3a045b33ca | |||
| a9438a9c2d | |||
| eea0a98090 | |||
| a491240aeb | |||
| 8c24a994e1 | |||
| 64f53e674c | |||
| 54fdf33fd9 | |||
| 997e9c5877 | |||
| e21c586cc5 | |||
| abedfd1015 | |||
| 2a0e1c0d3c | |||
| 5a6b71dda3 | |||
| a3dfa27a53 | |||
| cfb14ca80b | |||
| 0f79c4ff46 | |||
| 029dd99ae0 | |||
| a46366d336 | |||
| 07e8f79563 | |||
| 9ed7de50e7 | |||
| eed8e6b67a | |||
| 12fb90c232 | |||
| cda45f9bfb | |||
| ab4b8eca15 | |||
| 582cdcab82 | |||
| 5a589babcb | |||
| 0b8edb7671 | |||
| 9bd79ffc00 | |||
| c04dfdf38b | |||
| 2080aeee4d | |||
| 31f7e1aca3 | |||
| 1c4637cb43 | |||
| 559565d6a7 | |||
| ba38713fbc | |||
| 428b7b0c4e | |||
| 2f78bff97c | |||
| a85b147b3a | |||
| ee8e8a0c0f | |||
| d67cc00e4e | |||
| 63d252b603 | |||
| bd88a241ff | |||
| 76432341ed | |||
| ff2f4f8ed8 | |||
| 29c3a826c5 | |||
| 37e5b91dc2 | |||
| d67a6b7293 | |||
| 054df27929 | |||
| 2b1def4e7c | |||
| 08d64f1c25 | |||
| a7efd66ae9 | |||
| 92804fe4b2 | |||
| b07ed3f615 | |||
| 67b209808f | |||
| 82aabc63f5 | |||
| 17d7d9364f | |||
| 779f5d9b3d | |||
| 83198ef595 | |||
| 412c9bc76d | |||
| 72f9295490 | |||
| 1683bb75e1 | |||
| 727a8cd715 | |||
| 7489db9481 | |||
| 4733de25af | |||
| c24b6cf617 | |||
| 6bfa1fceec | |||
| 41504b9a2c | |||
| 3cd232f571 | |||
| 1b8844cb4a | |||
| a097e903c9 | |||
| def82a7354 | |||
| a94e45260e | |||
| bbed14a9ff | |||
| d09980f646 | |||
| e732cb68a7 | |||
| 4b4ebe4f80 | |||
| 7b5ff397b3 | |||
| 4b6206b32d | |||
| faab3aab0a | |||
| a1130b3e27 | |||
| de28f83d0d | |||
| b7e5407822 | |||
| 4023c665cc | |||
| 7aa75d5643 | |||
| cfece31649 | |||
| a326f7c833 | |||
| 078da36f20 | |||
| 6c8fad4cac | |||
| 8c3ff65402 | |||
| b33cb5fe3f | |||
| 84453d2e34 | |||
| d069ad5be4 | |||
| f1fa88a67e | |||
| 6aa4f2e67e | |||
| 053e8509a5 | |||
| d865e98e6f | |||
| 70d196dbdb | |||
| ea2755a559 | |||
| 932dd3c885 | |||
| 2de9a92fba | |||
| a41ffb5131 | |||
| ae34e4f59f | |||
| 8dc62bd29a | |||
| 4027cba4eb | |||
| 5bd597f14b | |||
| 49d9051879 | |||
| e5978981f3 | |||
| d257cdcbbf | |||
| 60c521101a | |||
| 11e7533a4d | |||
| ec8fb0be83 | |||
| a6cd4b8427 | |||
| 3bdd2612ce | |||
| 30b0b2474e | |||
| 8eb9dad989 | |||
| 3f1d37e556 | |||
| ba55e867e0 | |||
| 4fdb0835c9 | |||
| 430561d692 | |||
| e8fb529026 | |||
| 7a4ae7d142 | |||
| 9cb0a1ffbf | |||
| fa32c6660c | |||
| fe8c6b17a6 | |||
| 89f6190fb0 | |||
| ffdd504008 | |||
| 46597aac97 | |||
| 9b27a09131 | |||
| a50f125dd1 | |||
| 753842745d | |||
| 21caa06fa2 | |||
| 7a7475ed67 | |||
| dbb6a8dc2a | |||
| a5a27594b8 | |||
| 661409bac7 | |||
| f1a8e385e9 | |||
| a623556762 | |||
| 7dc5e0cc4f | |||
| ba5d5256b1 | |||
| 307ffc990d | |||
| 9b1a379fa6 | |||
| 4cb0f37918 | |||
| 3278dcbcbe | |||
| b733a29430 | |||
| 2dcd0e516f | |||
| e823b39579 | |||
| cd058fdafa | |||
| 1eea547aa2 | |||
| 4323d18387 | |||
| 1ec5d612fa | |||
| fcb990665c | |||
| ffaa08e7ea | |||
| 5dd11ca17a | |||
| 3da2b05428 | |||
| f88343019d | |||
| ba12d92af3 | |||
| 52a52f9f40 | |||
| 9125999d1a | |||
| 52dfe5fc92 | |||
| 4c0bb2308c | |||
| 4ef4cc8016 | |||
| 2f53f6a62c | |||
| ae46188753 | |||
| 51f6b8f23b | |||
| 5d377e5b0f | |||
| 972d5a3411 | |||
| 8df63b7c94 | |||
| ee3b2a0cf5 | |||
| 8988d3f886 | |||
| 4dc0fc45e7 | |||
| 1c93ef1916 | |||
| 9bf1d87e35 | |||
| 31b823058d | |||
| 9a2e0e8962 | |||
| e0ae936496 | |||
| 0675389aae | |||
| facd0bc3a4 | |||
| 967019d9e0 | |||
| e5da735918 | |||
| 9b3f60ffde | |||
| a5d19bc945 | |||
| 9995647d63 | |||
| 70881bc97f | |||
| dbf0ddf3a7 | |||
| 2e62e3a0ca | |||
| 6ab404597c | |||
| 5bc13c49a4 | |||
| 935ddf3fbd | |||
| 87c3d886ff | |||
| de71d8e0a3 | |||
| 7ef202c8b2 | |||
| e8b001f62f | |||
| 762c4684f8 | |||
| 2fa10a254c | |||
| 07c926bb12 | |||
| 3d410ff7dc | |||
| 2bb7b3e60f | |||
| 29a4389aac | |||
| 8ce18b3403 | |||
| fd3503e77d | |||
| ebe7a14c14 | |||
| f03381a5b1 | |||
| 8d44afe915 | |||
| db455060f0 | |||
| b63b42d3d7 | |||
| a4e6c43823 | |||
| 7303fab9d9 | |||
| 8b02f18e99 | |||
| 670a3838a3 | |||
| 3e06062974 | |||
| 3b772a772c | |||
| 55ecfafa82 | |||
| c89d91e006 | |||
| 15a4a4aaaa | |||
| 3d25d91e77 | |||
| efa6efd200 | |||
| 369acc7bea | |||
| 100363c7be | |||
| af0de1a768 | |||
| 09a7291527 | |||
| bb3d81bfc5 | |||
| f1331905f0 | |||
| 7eb8e2ff9c | |||
| dc7a329cc8 | |||
| 11de526bcf | |||
| 2e56e777ce | |||
| 6f53e83d49 | |||
| b1a896ba61 | |||
| d28abaad7b | |||
| 79442fc8a1 | |||
| 93f0a866a3 | |||
| 84fe41df31 | |||
| e4f32a045d | |||
| 784d92dbb3 | |||
| c88184673a | |||
| 74d431f881 | |||
| e2c0945bc1 | |||
| a02a24f349 | |||
| 87a7825cbc | |||
| f0ea99cea9 | |||
| 0d2a656aa1 |
@@ -1,30 +1,31 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
.github/
|
||||||
|
.git/
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
docs/
|
docs/
|
||||||
|
e2e/
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
server/node_modules/
|
cli/coverage/
|
||||||
|
cli/dist/
|
||||||
|
cli/node_modules/
|
||||||
|
|
||||||
|
open-api/typescript-sdk/build/
|
||||||
|
open-api/typescript-sdk/node_modules/
|
||||||
|
|
||||||
server/coverage/
|
server/coverage/
|
||||||
server/.reverse-geocoding-dump/
|
server/node_modules/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/dist/
|
server/dist/
|
||||||
|
server/www/
|
||||||
|
server/test/assets/
|
||||||
|
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/coverage/
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
web/build/
|
||||||
|
|
||||||
cli/node_modules/
|
|
||||||
cli/.reverse-geocoding-dump/
|
|
||||||
cli/upload/
|
|
||||||
cli/dist/
|
|
||||||
|
|
||||||
e2e/
|
|
||||||
|
|
||||||
open-api/typescript-sdk/node_modules/
|
|
||||||
open-api/typescript-sdk/build/
|
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ max_line_length = off
|
|||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
quote_type = double
|
quote_type = single
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
|||||||
mobile/lib/**/*.g.dart -diff -merge
|
mobile/lib/**/*.g.dart -diff -merge
|
||||||
mobile/lib/**/*.g.dart linguist-generated=true
|
mobile/lib/**/*.g.dart linguist-generated=true
|
||||||
|
|
||||||
open-api/typescript-sdk/axios-client/**/* -diff -merge
|
|
||||||
open-api/typescript-sdk/axios-client/**/* linguist-generated=true
|
|
||||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Please use this form to request new feature for Immich
|
Please use this form to request new feature for Immich
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: I have searched the existing feature requests to make sure this is not a duplicate request.
|
||||||
|
options:
|
||||||
|
- label: "Yes"
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: feature
|
id: feature
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.16.9"
|
flutter-version: "3.19.3"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.1.0
|
uses: docker/setup-buildx-action@v3.3.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
name: Update Immich SDK
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-sdk-repos:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GH_TOKEN }}
|
|
||||||
script: |
|
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
|
||||||
owner: 'immich-app',
|
|
||||||
repo: 'immich-sdk-typescript-axios',
|
|
||||||
workflow_id: 'build.yml',
|
|
||||||
ref: 'main'
|
|
||||||
})
|
|
||||||
@@ -66,13 +66,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.1.0
|
uses: docker/setup-buildx-action@v3.3.0
|
||||||
# Workaround to fix error:
|
|
||||||
# failed to push: failed to copy: io: read/write on closed pipe
|
|
||||||
# See https://github.com/docker/build-push-action/issues/761
|
|
||||||
with:
|
|
||||||
driver-opts: |
|
|
||||||
image=moby/buildkit:v0.10.6
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
# Only push to Docker Hub when making a release
|
# Only push to Docker Hub when making a release
|
||||||
@@ -121,7 +115,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.context }}
|
context: ${{ matrix.context }}
|
||||||
file: ${{ matrix.file }}
|
file: ${{ matrix.file }}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
serverBump:
|
serverBump:
|
||||||
description: "Bump server version"
|
description: 'Bump server version'
|
||||||
required: true
|
required: true
|
||||||
default: "false"
|
default: 'false'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- "false"
|
- 'false'
|
||||||
- minor
|
- minor
|
||||||
- patch
|
- patch
|
||||||
mobileBump:
|
mobileBump:
|
||||||
description: "Bump mobile build number"
|
description: 'Bump mobile build number'
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
author_name: Alex The Bot
|
author_name: Alex The Bot
|
||||||
author_email: alex.tran1502@gmail.com
|
author_email: alex.tran1502@gmail.com
|
||||||
default_author: user_info
|
default_author: user_info
|
||||||
message: "Version ${{ env.IMMICH_VERSION }}"
|
message: 'Version ${{ env.IMMICH_VERSION }}'
|
||||||
tag: ${{ env.IMMICH_VERSION }}
|
tag: ${{ env.IMMICH_VERSION }}
|
||||||
push: true
|
push: true
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
name: release-apk-signed
|
name: release-apk-signed
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ env.IMMICH_VERSION }}
|
tag_name: ${{ env.IMMICH_VERSION }}
|
||||||
@@ -85,4 +85,5 @@ jobs:
|
|||||||
docker/example.env
|
docker/example.env
|
||||||
docker/hwaccel.ml.yml
|
docker/hwaccel.ml.yml
|
||||||
docker/hwaccel.transcoding.yml
|
docker/hwaccel.transcoding.yml
|
||||||
|
docker/prometheus.yml
|
||||||
*.apk
|
*.apk
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: Update Immich SDK
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Publish `@immich/sdk`
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
# Setup .npmrc file to publish to npm
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
- name: Install deps
|
||||||
|
run: npm ci
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
- name: Publish
|
||||||
|
run: npm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.16.9"
|
flutter-version: "3.19.3"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: dart pub get
|
||||||
|
|||||||
@@ -10,23 +10,6 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
server-e2e-api:
|
|
||||||
name: Server (e2e-api)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./server
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Run npm install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run e2e tests
|
|
||||||
run: npm run e2e:api
|
|
||||||
|
|
||||||
server-e2e-jobs:
|
server-e2e-jobs:
|
||||||
name: Server (e2e-jobs)
|
name: Server (e2e-jobs)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -35,7 +18,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
run: make server-e2e-jobs
|
run: make server-e2e-jobs
|
||||||
@@ -108,17 +91,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
|
||||||
- name: Run npm install (cli)
|
- name: Install deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run npm install (server)
|
|
||||||
run: npm ci
|
|
||||||
working-directory: ./server
|
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
@@ -184,7 +163,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -194,25 +173,44 @@ jobs:
|
|||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run setup cli
|
- name: Run setup cli
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run formatter
|
||||||
|
run: npm run format
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run tsc
|
||||||
|
run: npm run check
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps chromium
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose build
|
run: docker compose build
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: npm run test
|
run: npm run test
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Mobile
|
name: Mobile
|
||||||
@@ -222,8 +220,8 @@ jobs:
|
|||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: 'stable'
|
||||||
flutter-version: "3.16.9"
|
flutter-version: '3.16.9'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
@@ -241,7 +239,7 @@ jobs:
|
|||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: "poetry"
|
cache: 'poetry'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
poetry install --with dev --with cpu
|
poetry install --with dev --with cpu
|
||||||
@@ -279,7 +277,7 @@ jobs:
|
|||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v18
|
uses: tj-actions/verify-changed-files@v19
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -331,14 +329,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
run: npm run typeorm:migrations:generate ./src/migrations/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v18
|
uses: tj-actions/verify-changed-files@v19
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
server/src/infra/migrations/
|
server/src/migrations/
|
||||||
- name: Verify migration files have not changed
|
- name: Verify migration files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -352,11 +350,11 @@ jobs:
|
|||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v18
|
uses: tj-actions/verify-changed-files@v19
|
||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
server/src/infra/sql
|
server/src/queries
|
||||||
|
|
||||||
- name: Verify SQL files have not changed
|
- name: Verify SQL files have not changed
|
||||||
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript][typescript][css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"svelte"
|
||||||
|
],
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
|
"[dart]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.selectionHighlight": false,
|
||||||
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
|
"editor.suggestSelection": "first",
|
||||||
|
"editor.tabCompletion": "onlySnippets",
|
||||||
|
"editor.wordBasedSuggestions": "off",
|
||||||
|
"editor.defaultFormatter": "Dart-Code.dart-code"
|
||||||
|
},
|
||||||
|
"cSpell.words": [
|
||||||
|
"immich"
|
||||||
|
],
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/.github/ @bo0tzz
|
||||||
|
/docker/ @bo0tzz
|
||||||
|
/server/ @danieldietzler
|
||||||
|
/machine-learning/ @mertalev
|
||||||
|
/e2e/ @danieldietzler
|
||||||
@@ -19,9 +19,6 @@ pull-stage:
|
|||||||
server-e2e-jobs:
|
server-e2e-jobs:
|
||||||
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||||
|
|
||||||
server-e2e-api:
|
|
||||||
npm run e2e:api --prefix server
|
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|||||||
@@ -9,26 +9,26 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
|
<img src="design/immich-logo-stacked-light.svg" width="300" title="Login With Custom URL">
|
||||||
</p>
|
</p>
|
||||||
<h3 align="center">Immich - High performance self-hosted photo and video backup solution</h3>
|
<h3 align="center">High performance self-hosted photo and video management solution</h3>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://immich.app">
|
<a href="https://immich.app">
|
||||||
<img src="design/immich-screenshots.png" title="Main Screenshot">
|
<img src="design/immich-screenshots.png" title="Main Screenshot">
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="readme_i18n/README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="readme_i18n/README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="readme_i18n/README_fr_FR.md">Français</a>
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
<a href="readme_i18n/README_it_IT.md">Italiano</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="readme_i18n/README_ja_JP.md">日本語</a>
|
||||||
<a href="README_ko_KR.md">한국어</a>
|
<a href="readme_i18n/README_ko_KR.md">한국어</a>
|
||||||
<a href="README_de_DE.md">Deutsch</a>
|
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_tr_TR.md">Türkçe</a>
|
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
|
||||||
<a href="README_zh_CN.md">中文</a>
|
<a href="readme_i18n/README_zh_CN.md">中文</a>
|
||||||
<a href="README_ru_RU.md">Русский</a>
|
<a href="readme_i18n/README_ru_RU.md">Русский</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
@@ -131,6 +131,10 @@ If you feel like this is the right cause and the app is something you are seeing
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#immich-app/immich">
|
<a href="https://star-history.com/#immich-app/immich&Date">
|
||||||
<img src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" alt="Star History Chart" width="100%" />
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
||||||
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ module.exports = {
|
|||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
'unicorn/prefer-module': 'off',
|
'unicorn/prefer-module': 'off',
|
||||||
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
|
'unicorn/no-process-exit': 'off',
|
||||||
|
'unicorn/import-style': 'off',
|
||||||
curly: 2,
|
curly: 2,
|
||||||
'prettier/prettier': 0,
|
'prettier/prettier': 0,
|
||||||
'unicorn/prevent-abbreviations': 'error',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
**/*.spec.js
|
**/*.spec.js
|
||||||
|
coverage/**
|
||||||
|
src/**
|
||||||
upload/**
|
upload/**
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.eslintignore
|
.eslintignore
|
||||||
.eslintrc.js
|
.eslintrc.cjs
|
||||||
|
.gitignore
|
||||||
.prettierignore
|
.prettierignore
|
||||||
.prettierrc
|
.prettierrc
|
||||||
|
Dockerfile
|
||||||
package-lock.json
|
package-lock.json
|
||||||
testSetup.js
|
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
tsconfig.build.json
|
vite.config.ts
|
||||||
|
vitest.config.ts
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine3.19@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c as core
|
FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.0.8",
|
"version": "2.2.0",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@testcontainers/postgresql": "^10.7.1",
|
|
||||||
"@types/byte-size": "^8.1.0",
|
"@types/byte-size": "^8.1.0",
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
@@ -29,13 +28,14 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^51.0.0",
|
"eslint-plugin-unicorn": "^52.0.0",
|
||||||
"glob": "^10.3.1",
|
"glob": "^10.3.1",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.0.12",
|
"vite": "^5.0.12",
|
||||||
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest": "^1.2.2",
|
"vitest": "^1.2.2",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
import {
|
||||||
|
Action,
|
||||||
|
AssetBulkUploadCheckResult,
|
||||||
|
AssetFileUploadResponseDto,
|
||||||
|
addAssetsToAlbum,
|
||||||
|
checkBulkUpload,
|
||||||
|
createAlbum,
|
||||||
|
defaults,
|
||||||
|
getAllAlbums,
|
||||||
|
getSupportedMediaTypes,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import byteSize from 'byte-size';
|
||||||
|
import { Presets, SingleBar } from 'cli-progress';
|
||||||
|
import { chunk } from 'lodash-es';
|
||||||
|
import { Stats, createReadStream } from 'node:fs';
|
||||||
|
import { stat, unlink } from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path, { basename } from 'node:path';
|
||||||
|
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
|
||||||
|
|
||||||
|
const s = (count: number) => (count === 1 ? '' : 's');
|
||||||
|
|
||||||
|
// TODO figure out why `id` is missing
|
||||||
|
type AssetBulkUploadCheckResults = Array<AssetBulkUploadCheckResult & { id: string }>;
|
||||||
|
type Asset = { id: string; filepath: string };
|
||||||
|
|
||||||
|
interface UploadOptionsDto {
|
||||||
|
recursive?: boolean;
|
||||||
|
exclusionPatterns?: string[];
|
||||||
|
dryRun?: boolean;
|
||||||
|
skipHash?: boolean;
|
||||||
|
delete?: boolean;
|
||||||
|
album?: boolean;
|
||||||
|
albumName?: string;
|
||||||
|
includeHidden?: boolean;
|
||||||
|
concurrency: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadFile extends File {
|
||||||
|
constructor(
|
||||||
|
private filepath: string,
|
||||||
|
private _size: number,
|
||||||
|
) {
|
||||||
|
super([], basename(filepath));
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream() {
|
||||||
|
return createReadStream(this.filepath) as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
|
||||||
|
await authenticate(baseOptions);
|
||||||
|
|
||||||
|
const scanFiles = await scan(paths, options);
|
||||||
|
if (scanFiles.length === 0) {
|
||||||
|
console.log('No files found, exiting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newFiles, duplicates } = await checkForDuplicates(scanFiles, options);
|
||||||
|
const newAssets = await uploadFiles(newFiles, options);
|
||||||
|
await updateAlbums([...newAssets, ...duplicates], options);
|
||||||
|
await deleteFiles(newFiles, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
||||||
|
const { image, video } = await getSupportedMediaTypes();
|
||||||
|
|
||||||
|
console.log('Crawling for assets...');
|
||||||
|
const files = await crawl({
|
||||||
|
pathsToCrawl,
|
||||||
|
recursive: options.recursive,
|
||||||
|
exclusionPatterns: options.exclusionPatterns,
|
||||||
|
includeHidden: options.includeHidden,
|
||||||
|
extensions: [...image, ...video],
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
|
||||||
|
if (skipHash) {
|
||||||
|
console.log('Skipping hash check, assuming all files are new');
|
||||||
|
return { newFiles: files, duplicates: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressBar = new SingleBar(
|
||||||
|
{ format: 'Checking files | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
|
||||||
|
progressBar.start(files.length, 0);
|
||||||
|
|
||||||
|
const newFiles: string[] = [];
|
||||||
|
const duplicates: Asset[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO refactor into a queue
|
||||||
|
for (const items of chunk(files, concurrency)) {
|
||||||
|
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })));
|
||||||
|
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
|
||||||
|
|
||||||
|
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) {
|
||||||
|
if (action === Action.Accept) {
|
||||||
|
newFiles.push(filepath);
|
||||||
|
} else {
|
||||||
|
// rejects are always duplicates
|
||||||
|
duplicates.push({ id: assetId as string, filepath });
|
||||||
|
}
|
||||||
|
progressBar.increment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
progressBar.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
|
||||||
|
|
||||||
|
return { newFiles, duplicates };
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('All assets were already uploaded, nothing to do.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute total size first
|
||||||
|
let totalSize = 0;
|
||||||
|
const statsMap = new Map<string, Stats>();
|
||||||
|
for (const filepath of files) {
|
||||||
|
const stats = await stat(filepath);
|
||||||
|
statsMap.set(filepath, stats);
|
||||||
|
totalSize += stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
console.log(`Would have uploaded ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadProgress = new SingleBar(
|
||||||
|
{ format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
uploadProgress.start(totalSize, 0);
|
||||||
|
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||||
|
|
||||||
|
let duplicateCount = 0;
|
||||||
|
let duplicateSize = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
let successSize = 0;
|
||||||
|
|
||||||
|
const newAssets: Asset[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const items of chunk(files, concurrency)) {
|
||||||
|
await Promise.all(
|
||||||
|
items.map(async (filepath) => {
|
||||||
|
const stats = statsMap.get(filepath) as Stats;
|
||||||
|
const response = await uploadFile(filepath, stats);
|
||||||
|
|
||||||
|
newAssets.push({ id: response.id, filepath });
|
||||||
|
|
||||||
|
if (response.duplicate) {
|
||||||
|
duplicateCount++;
|
||||||
|
duplicateSize += stats.size ?? 0;
|
||||||
|
} else {
|
||||||
|
successCount++;
|
||||||
|
successSize += stats.size ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uploadProgress.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
|
||||||
|
if (duplicateCount > 0) {
|
||||||
|
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
|
||||||
|
}
|
||||||
|
return newAssets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async (input: string, stats: Stats): Promise<AssetFileUploadResponseDto> => {
|
||||||
|
const { baseUrl, headers } = defaults;
|
||||||
|
|
||||||
|
const assetPath = path.parse(input);
|
||||||
|
const noExtension = path.join(assetPath.dir, assetPath.name);
|
||||||
|
|
||||||
|
const sidecarsFiles = await Promise.all(
|
||||||
|
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||||
|
[`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => {
|
||||||
|
try {
|
||||||
|
const stats = await stat(sidecarPath);
|
||||||
|
return new UploadFile(sidecarPath, stats.size);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
||||||
|
formData.append('deviceId', 'CLI');
|
||||||
|
formData.append('fileCreatedAt', stats.mtime.toISOString());
|
||||||
|
formData.append('fileModifiedAt', stats.mtime.toISOString());
|
||||||
|
formData.append('fileSize', String(stats.size));
|
||||||
|
formData.append('isFavorite', 'false');
|
||||||
|
formData.append('assetData', new UploadFile(input, stats.size));
|
||||||
|
|
||||||
|
if (sidecarData) {
|
||||||
|
formData.append('sidecarData', sidecarData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/asset/upload`, {
|
||||||
|
method: 'post',
|
||||||
|
redirect: 'error',
|
||||||
|
headers: headers as Record<string, string>,
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (response.status !== 200 && response.status !== 201) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFiles = async (files: string[], options: UploadOptionsDto): Promise<void> => {
|
||||||
|
if (!options.delete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.dryRun) {
|
||||||
|
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Deleting assets that have been uploaded...');
|
||||||
|
|
||||||
|
const deletionProgress = new SingleBar(
|
||||||
|
{ format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
deletionProgress.start(files.length, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const assetBatch of chunk(files, options.concurrency)) {
|
||||||
|
await Promise.all(assetBatch.map((input: string) => unlink(input)));
|
||||||
|
deletionProgress.update(assetBatch.length);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deletionProgress.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
|
||||||
|
if (!options.album && !options.albumName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { dryRun, concurrency } = options;
|
||||||
|
|
||||||
|
const albums = await getAllAlbums({});
|
||||||
|
const existingAlbums = new Map(albums.map((album) => [album.albumName, album.id]));
|
||||||
|
const newAlbums: Set<string> = new Set();
|
||||||
|
for (const { filepath } of assets) {
|
||||||
|
const albumName = getAlbumName(filepath, options);
|
||||||
|
if (albumName && !existingAlbums.has(albumName)) {
|
||||||
|
newAlbums.add(albumName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
// TODO print asset counts for new albums
|
||||||
|
console.log(`Would have created ${newAlbums.size} new album${s(newAlbums.size)}`);
|
||||||
|
console.log(`Would have updated ${assets.length} asset${s(assets.length)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressBar = new SingleBar(
|
||||||
|
{ format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
progressBar.start(newAlbums.size, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const albumNames of chunk([...newAlbums], concurrency)) {
|
||||||
|
const items = await Promise.all(
|
||||||
|
albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } })),
|
||||||
|
);
|
||||||
|
for (const { id, albumName } of items) {
|
||||||
|
existingAlbums.set(albumName, id);
|
||||||
|
}
|
||||||
|
progressBar.increment(albumNames.length);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
progressBar.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully created ${newAlbums.size} new album${s(newAlbums.size)}`);
|
||||||
|
console.log(`Successfully updated ${assets.length} asset${s(assets.length)}`);
|
||||||
|
|
||||||
|
const albumToAssets = new Map<string, string[]>();
|
||||||
|
for (const asset of assets) {
|
||||||
|
const albumName = getAlbumName(asset.filepath, options);
|
||||||
|
if (!albumName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const albumId = existingAlbums.get(albumName);
|
||||||
|
if (albumId) {
|
||||||
|
if (!albumToAssets.has(albumId)) {
|
||||||
|
albumToAssets.set(albumId, []);
|
||||||
|
}
|
||||||
|
albumToAssets.get(albumId)?.push(asset.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumUpdateProgress = new SingleBar(
|
||||||
|
{ format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
albumUpdateProgress.start(assets.length, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const [albumId, assets] of albumToAssets.entries()) {
|
||||||
|
for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) {
|
||||||
|
await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } });
|
||||||
|
albumUpdateProgress.increment(assetBatch.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
albumUpdateProgress.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlbumName = (filepath: string, options: UploadOptionsDto) => {
|
||||||
|
const folderName = os.platform() === 'win32' ? filepath.split('\\').at(-2) : filepath.split('/').at(-2);
|
||||||
|
return options.albumName ?? folderName;
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { getMyUserInfo } 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';
|
||||||
|
|
||||||
|
export const login = async (url: string, key: string, options: BaseOptions) => {
|
||||||
|
console.log(`Logging in to ${url}`);
|
||||||
|
|
||||||
|
const { configDirectory: configDir } = options;
|
||||||
|
|
||||||
|
await connect(url, key);
|
||||||
|
|
||||||
|
const [error, userInfo] = await withError(getMyUserInfo());
|
||||||
|
if (error) {
|
||||||
|
logError(error, 'Failed to load user info');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Logged in as ${userInfo.email}`);
|
||||||
|
|
||||||
|
if (!existsSync(configDir)) {
|
||||||
|
// Create config folder if it doesn't exist
|
||||||
|
const created = await mkdir(configDir, { recursive: true });
|
||||||
|
if (!created) {
|
||||||
|
console.log(`Failed to create config folder: ${configDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeAuthFile(configDir, { url, key });
|
||||||
|
|
||||||
|
console.log(`Wrote auth info to ${getAuthFilePath(configDir)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = async (options: BaseOptions) => {
|
||||||
|
console.log('Logging out...');
|
||||||
|
|
||||||
|
const { configDirectory: configDir } = options;
|
||||||
|
|
||||||
|
const authFile = getAuthFilePath(configDir);
|
||||||
|
|
||||||
|
if (existsSync(authFile)) {
|
||||||
|
await unlink(authFile);
|
||||||
|
console.log(`Removed auth file: ${authFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Successfully logged out');
|
||||||
|
};
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk';
|
|
||||||
import { ImmichApi } from 'src/services/api.service';
|
|
||||||
import { SessionService } from '../services/session.service';
|
|
||||||
|
|
||||||
export abstract class BaseCommand {
|
|
||||||
protected sessionService!: SessionService;
|
|
||||||
protected user!: UserResponseDto;
|
|
||||||
protected serverVersion!: ServerVersionResponseDto;
|
|
||||||
|
|
||||||
constructor(options: { configDirectory?: string }) {
|
|
||||||
if (!options.configDirectory) {
|
|
||||||
throw new Error('Config directory is required');
|
|
||||||
}
|
|
||||||
this.sessionService = new SessionService(options.configDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async connect(): Promise<ImmichApi> {
|
|
||||||
return await this.sessionService.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { BaseCommand } from './base-command';
|
|
||||||
|
|
||||||
export class LoginCommand extends BaseCommand {
|
|
||||||
public async run(instanceUrl: string, apiKey: string): Promise<void> {
|
|
||||||
await this.sessionService.login(instanceUrl, apiKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { BaseCommand } from './base-command';
|
|
||||||
|
|
||||||
export class LogoutCommand extends BaseCommand {
|
|
||||||
public static readonly description = 'Logout and remove persisted credentials';
|
|
||||||
public async run(): Promise<void> {
|
|
||||||
await this.sessionService.logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { BaseCommand } from './base-command';
|
|
||||||
|
|
||||||
export class ServerInfoCommand extends BaseCommand {
|
|
||||||
public async run() {
|
|
||||||
const api = await this.connect();
|
|
||||||
const versionInfo = await api.getServerVersion();
|
|
||||||
const mediaTypes = await api.getSupportedMediaTypes();
|
|
||||||
const statistics = await api.getAssetStatistics();
|
|
||||||
|
|
||||||
console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
|
||||||
console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
|
|
||||||
console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`);
|
|
||||||
console.log(
|
|
||||||
`Statistics:\n Images: ${statistics.images}\n Videos: ${statistics.videos}\n Total: ${statistics.total}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
||||||
|
import { BaseOptions, authenticate } from 'src/utils';
|
||||||
|
|
||||||
|
export const serverInfo = async (options: BaseOptions) => {
|
||||||
|
const { url } = await authenticate(options);
|
||||||
|
|
||||||
|
const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([
|
||||||
|
getServerVersion(),
|
||||||
|
getSupportedMediaTypes(),
|
||||||
|
getAssetStatistics({}),
|
||||||
|
getMyUserInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`Server Info (via ${userInfo.email})`);
|
||||||
|
console.log(` Url: ${url}`);
|
||||||
|
console.log(` Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
||||||
|
console.log(` Formats:`);
|
||||||
|
console.log(` Images: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
|
||||||
|
console.log(` Videos: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`);
|
||||||
|
console.log(` Statistics:`);
|
||||||
|
console.log(` Images: ${stats.images}`);
|
||||||
|
console.log(` Videos: ${stats.videos}`);
|
||||||
|
console.log(` Total: ${stats.total}`);
|
||||||
|
};
|
||||||
@@ -1,444 +0,0 @@
|
|||||||
import { AssetBulkUploadCheckResult } from '@immich/sdk';
|
|
||||||
import byteSize from 'byte-size';
|
|
||||||
import cliProgress from 'cli-progress';
|
|
||||||
import { chunk, zip } from 'lodash-es';
|
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import fs, { createReadStream } from 'node:fs';
|
|
||||||
import { access, constants, stat, unlink } from 'node:fs/promises';
|
|
||||||
import os from 'node:os';
|
|
||||||
import { basename } from 'node:path';
|
|
||||||
import { ImmichApi } from 'src/services/api.service';
|
|
||||||
import { CrawlService } from '../services/crawl.service';
|
|
||||||
import { BaseCommand } from './base-command';
|
|
||||||
|
|
||||||
const zipDefined = zip as <T, U>(a: T[], b: U[]) => [T, U][];
|
|
||||||
|
|
||||||
enum CheckResponseStatus {
|
|
||||||
ACCEPT = 'accept',
|
|
||||||
REJECT = 'reject',
|
|
||||||
DUPLICATE = 'duplicate',
|
|
||||||
}
|
|
||||||
|
|
||||||
class Asset {
|
|
||||||
readonly path: string;
|
|
||||||
|
|
||||||
id?: string;
|
|
||||||
deviceAssetId?: string;
|
|
||||||
fileCreatedAt?: Date;
|
|
||||||
fileModifiedAt?: Date;
|
|
||||||
sidecarPath?: string;
|
|
||||||
fileSize?: number;
|
|
||||||
albumName?: string;
|
|
||||||
|
|
||||||
constructor(path: string) {
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
async prepare() {
|
|
||||||
const stats = await stat(this.path);
|
|
||||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, '');
|
|
||||||
this.fileCreatedAt = stats.mtime;
|
|
||||||
this.fileModifiedAt = stats.mtime;
|
|
||||||
this.fileSize = stats.size;
|
|
||||||
this.albumName = this.extractAlbumName();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUploadFormData(): Promise<FormData> {
|
|
||||||
if (!this.deviceAssetId) {
|
|
||||||
throw new Error('Device asset id not set');
|
|
||||||
}
|
|
||||||
if (!this.fileCreatedAt) {
|
|
||||||
throw new Error('File created at not set');
|
|
||||||
}
|
|
||||||
if (!this.fileModifiedAt) {
|
|
||||||
throw new Error('File modified at not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: doesn't xmp replace the file extension? Will need investigation
|
|
||||||
const sideCarPath = `${this.path}.xmp`;
|
|
||||||
let sidecarData: Blob | undefined = undefined;
|
|
||||||
try {
|
|
||||||
await access(sideCarPath, constants.R_OK);
|
|
||||||
sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath));
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const data: any = {
|
|
||||||
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
|
||||||
deviceAssetId: this.deviceAssetId,
|
|
||||||
deviceId: 'CLI',
|
|
||||||
fileCreatedAt: this.fileCreatedAt,
|
|
||||||
fileModifiedAt: this.fileModifiedAt,
|
|
||||||
isFavorite: String(false),
|
|
||||||
};
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
for (const property in data) {
|
|
||||||
formData.append(property, data[property]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sidecarData) {
|
|
||||||
formData.append('sidecarData', sidecarData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formData;
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
|
||||||
return unlink(this.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async hash(): Promise<string> {
|
|
||||||
const sha1 = (filePath: string) => {
|
|
||||||
const hash = createHash('sha1');
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
const rs = createReadStream(filePath);
|
|
||||||
rs.on('error', reject);
|
|
||||||
rs.on('data', (chunk) => hash.update(chunk));
|
|
||||||
rs.on('end', () => resolve(hash.digest('hex')));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return await sha1(this.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractAlbumName(): string | undefined {
|
|
||||||
return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UploadOptionsDto {
|
|
||||||
recursive? = false;
|
|
||||||
exclusionPatterns?: string[] = [];
|
|
||||||
dryRun? = false;
|
|
||||||
skipHash? = false;
|
|
||||||
delete? = false;
|
|
||||||
album? = false;
|
|
||||||
albumName? = '';
|
|
||||||
includeHidden? = false;
|
|
||||||
concurrency? = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UploadCommand extends BaseCommand {
|
|
||||||
api!: ImmichApi;
|
|
||||||
|
|
||||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
|
||||||
this.api = await this.connect();
|
|
||||||
|
|
||||||
console.log('Crawling for assets...');
|
|
||||||
const files = await this.getFiles(paths, options);
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
console.log('No assets found, exiting');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsToCheck = files.map((path) => new Asset(path));
|
|
||||||
|
|
||||||
const { newAssets, duplicateAssets } = await this.checkAssets(assetsToCheck, options.concurrency ?? 4);
|
|
||||||
|
|
||||||
const totalSizeUploaded = await this.upload(newAssets, options);
|
|
||||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
|
||||||
if (newAssets.length === 0) {
|
|
||||||
console.log('All assets were already uploaded, nothing to do.');
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${messageStart} uploaded ${newAssets.length} asset${newAssets.length === 1 ? '' : 's'} (${byteSize(totalSizeUploaded)})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.album || options.albumName) {
|
|
||||||
const { createdAlbumCount, updatedAssetCount } = await this.updateAlbums(
|
|
||||||
[...newAssets, ...duplicateAssets],
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
console.log(`${messageStart} created ${createdAlbumCount} new album${createdAlbumCount === 1 ? '' : 's'}`);
|
|
||||||
console.log(`${messageStart} updated ${updatedAssetCount} asset${updatedAssetCount === 1 ? '' : 's'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.delete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Deleting assets that have been uploaded...');
|
|
||||||
|
|
||||||
await this.deleteAssets(newAssets, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkAssets(
|
|
||||||
assetsToCheck: Asset[],
|
|
||||||
concurrency: number,
|
|
||||||
): Promise<{ newAssets: Asset[]; duplicateAssets: Asset[]; rejectedAssets: Asset[] }> {
|
|
||||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
|
||||||
await Promise.all(assets.map((asset: Asset) => asset.prepare()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkProgress = new cliProgress.SingleBar(
|
|
||||||
{ format: 'Checking assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
checkProgress.start(assetsToCheck.length, 0);
|
|
||||||
|
|
||||||
const newAssets = [];
|
|
||||||
const duplicateAssets = [];
|
|
||||||
const rejectedAssets = [];
|
|
||||||
try {
|
|
||||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
|
||||||
const checkedAssets = await this.getStatus(assets);
|
|
||||||
for (const checked of checkedAssets) {
|
|
||||||
if (checked.status === CheckResponseStatus.ACCEPT) {
|
|
||||||
newAssets.push(checked.asset);
|
|
||||||
} else if (checked.status === CheckResponseStatus.DUPLICATE) {
|
|
||||||
duplicateAssets.push(checked.asset);
|
|
||||||
} else {
|
|
||||||
rejectedAssets.push(checked.asset);
|
|
||||||
}
|
|
||||||
checkProgress.increment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
checkProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { newAssets, duplicateAssets, rejectedAssets };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async upload(assetsToUpload: Asset[], options: UploadOptionsDto): Promise<number> {
|
|
||||||
let totalSize = 0;
|
|
||||||
|
|
||||||
// Compute total size first
|
|
||||||
for (const asset of assetsToUpload) {
|
|
||||||
totalSize += asset.fileSize ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
return totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
uploadProgress.start(totalSize, 0);
|
|
||||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
|
||||||
|
|
||||||
let totalSizeUploaded = 0;
|
|
||||||
try {
|
|
||||||
for (const assets of chunk(assetsToUpload, options.concurrency)) {
|
|
||||||
const ids = await this.uploadAssets(assets);
|
|
||||||
for (const [asset, id] of zipDefined(assets, ids)) {
|
|
||||||
asset.id = id;
|
|
||||||
if (asset.fileSize) {
|
|
||||||
totalSizeUploaded += asset.fileSize ?? 0;
|
|
||||||
} else {
|
|
||||||
console.log(`Could not determine file size for ${asset.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
uploadProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalSizeUploaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getFiles(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
|
||||||
const inputFiles: string[] = [];
|
|
||||||
for (const pathArgument of paths) {
|
|
||||||
const fileStat = await fs.promises.lstat(pathArgument);
|
|
||||||
if (fileStat.isFile()) {
|
|
||||||
inputFiles.push(pathArgument);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = await this.crawl(paths, options);
|
|
||||||
files.push(...inputFiles);
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAlbums(): Promise<Map<string, string>> {
|
|
||||||
const existingAlbums = await this.api.getAllAlbums();
|
|
||||||
|
|
||||||
const albumMapping = new Map<string, string>();
|
|
||||||
for (const album of existingAlbums) {
|
|
||||||
albumMapping.set(album.albumName, album.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return albumMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateAlbums(
|
|
||||||
assets: Asset[],
|
|
||||||
options: UploadOptionsDto,
|
|
||||||
): Promise<{ createdAlbumCount: number; updatedAssetCount: number }> {
|
|
||||||
if (options.albumName) {
|
|
||||||
for (const asset of assets) {
|
|
||||||
asset.albumName = options.albumName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingAlbums = await this.getAlbums();
|
|
||||||
const assetsToUpdate = assets.filter(
|
|
||||||
(asset): asset is Asset & { albumName: string; id: string } => !!(asset.albumName && asset.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newAlbumsSet: Set<string> = new Set();
|
|
||||||
for (const asset of assetsToUpdate) {
|
|
||||||
if (!existingAlbums.has(asset.albumName)) {
|
|
||||||
newAlbumsSet.add(asset.albumName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAlbums = [...newAlbumsSet];
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
const albumCreationProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
albumCreationProgress.start(newAlbums.length, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const albumNames of chunk(newAlbums, options.concurrency)) {
|
|
||||||
const newAlbumIds = await Promise.all(
|
|
||||||
albumNames.map((albumName: string) => this.api.createAlbum({ albumName }).then((r) => r.id)),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) {
|
|
||||||
existingAlbums.set(albumName, albumId);
|
|
||||||
}
|
|
||||||
|
|
||||||
albumCreationProgress.increment(albumNames.length);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
albumCreationProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const albumToAssets = new Map<string, string[]>();
|
|
||||||
for (const asset of assetsToUpdate) {
|
|
||||||
const albumId = existingAlbums.get(asset.albumName);
|
|
||||||
if (albumId) {
|
|
||||||
if (!albumToAssets.has(albumId)) {
|
|
||||||
albumToAssets.set(albumId, []);
|
|
||||||
}
|
|
||||||
albumToAssets.get(albumId)?.push(asset.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const albumUpdateProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
albumUpdateProgress.start(assetsToUpdate.length, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const [albumId, assets] of albumToAssets.entries()) {
|
|
||||||
for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) {
|
|
||||||
await this.api.addAssetsToAlbum(albumId, { ids: assetBatch });
|
|
||||||
albumUpdateProgress.increment(assetBatch.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
albumUpdateProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteAssets(assets: Asset[], options: UploadOptionsDto): Promise<void> {
|
|
||||||
const deletionProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
deletionProgress.start(assets.length, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const assetBatch of chunk(assets, options.concurrency)) {
|
|
||||||
await Promise.all(assetBatch.map((asset: Asset) => asset.delete()));
|
|
||||||
deletionProgress.update(assetBatch.length);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
deletionProgress.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getStatus(assets: Asset[]): Promise<{ asset: Asset; status: CheckResponseStatus }[]> {
|
|
||||||
const checkResponse = await this.checkHashes(assets);
|
|
||||||
|
|
||||||
const responses = [];
|
|
||||||
for (const [check, asset] of zipDefined(checkResponse, assets)) {
|
|
||||||
if (check.assetId) {
|
|
||||||
asset.id = check.assetId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check.action === 'accept') {
|
|
||||||
responses.push({ asset, status: CheckResponseStatus.ACCEPT });
|
|
||||||
} else if (check.reason === 'duplicate') {
|
|
||||||
responses.push({ asset, status: CheckResponseStatus.DUPLICATE });
|
|
||||||
} else {
|
|
||||||
responses.push({ asset, status: CheckResponseStatus.REJECT });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return responses;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkHashes(assetsToCheck: Asset[]): Promise<AssetBulkUploadCheckResult[]> {
|
|
||||||
const checksums = await Promise.all(assetsToCheck.map((asset) => asset.hash()));
|
|
||||||
const assetBulkUploadCheckDto = {
|
|
||||||
assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })),
|
|
||||||
};
|
|
||||||
const checkResponse = await this.api.checkBulkUpload(assetBulkUploadCheckDto);
|
|
||||||
return checkResponse.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadAssets(assets: Asset[]): Promise<string[]> {
|
|
||||||
const fileRequests = await Promise.all(assets.map((asset) => asset.getUploadFormData()));
|
|
||||||
return Promise.all(fileRequests.map((request) => this.uploadAsset(request).then((response) => response.id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async crawl(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
|
||||||
const formatResponse = await this.api.getSupportedMediaTypes();
|
|
||||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
|
||||||
|
|
||||||
return crawlService.crawl({
|
|
||||||
pathsToCrawl: paths,
|
|
||||||
recursive: options.recursive,
|
|
||||||
exclusionPatterns: options.exclusionPatterns,
|
|
||||||
includeHidden: options.includeHidden,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadAsset(data: FormData): Promise<{ id: string }> {
|
|
||||||
const url = this.api.instanceUrl + '/asset/upload';
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'post',
|
|
||||||
redirect: 'error',
|
|
||||||
headers: {
|
|
||||||
'x-api-key': this.api.apiKey,
|
|
||||||
},
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
if (response.status !== 200 && response.status !== 201) {
|
|
||||||
throw new Error(await response.text());
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
import { Command, Option } from 'commander';
|
import { Command, Option } from 'commander';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { upload } from 'src/commands/asset';
|
||||||
|
import { login, logout } from 'src/commands/auth';
|
||||||
|
import { serverInfo } from 'src/commands/server-info';
|
||||||
import { version } from '../package.json';
|
import { version } from '../package.json';
|
||||||
import { LoginCommand } from './commands/login.command';
|
|
||||||
import { LogoutCommand } from './commands/logout.command';
|
|
||||||
import { ServerInfoCommand } from './commands/server-info.command';
|
|
||||||
import { UploadCommand } from './commands/upload.command';
|
|
||||||
|
|
||||||
const defaultConfigDirectory = path.join(os.homedir(), '.config/immich/');
|
const defaultConfigDirectory = path.join(os.homedir(), '.config/immich/');
|
||||||
|
|
||||||
@@ -18,14 +17,34 @@ const program = new Command()
|
|||||||
new Option('-d, --config-directory <directory>', 'Configuration directory where auth.yml will be stored')
|
new Option('-d, --config-directory <directory>', 'Configuration directory where auth.yml will be stored')
|
||||||
.env('IMMICH_CONFIG_DIR')
|
.env('IMMICH_CONFIG_DIR')
|
||||||
.default(defaultConfigDirectory),
|
.default(defaultConfigDirectory),
|
||||||
);
|
)
|
||||||
|
.addOption(new Option('-u, --url [url]', 'Immich server URL').env('IMMICH_INSTANCE_URL'))
|
||||||
|
.addOption(new Option('-k, --key [key]', 'Immich API key').env('IMMICH_API_KEY'));
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('login')
|
||||||
|
.alias('login-key')
|
||||||
|
.description('Login using an API key')
|
||||||
|
.argument('url', 'Immich server URL')
|
||||||
|
.argument('key', 'Immich API key')
|
||||||
|
.action((url, key) => login(url, key, program.opts()));
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('logout')
|
||||||
|
.description('Remove stored credentials')
|
||||||
|
.action(() => logout(program.opts()));
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('server-info')
|
||||||
|
.description('Display server information')
|
||||||
|
.action(() => serverInfo(program.opts()));
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('upload')
|
.command('upload')
|
||||||
.description('Upload assets')
|
.description('Upload assets')
|
||||||
.usage('[options] [paths...]')
|
.usage('[paths...] [options]')
|
||||||
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
|
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
|
||||||
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS'))
|
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default([]))
|
||||||
.addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
|
.addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
|
||||||
.addOption(new Option('-H, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false))
|
.addOption(new Option('-H, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false))
|
||||||
.addOption(
|
.addOption(
|
||||||
@@ -44,38 +63,12 @@ program
|
|||||||
.default(false),
|
.default(false),
|
||||||
)
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('-c, --concurrency', 'Number of assets to upload at the same time')
|
new Option('-c, --concurrency <number>', 'Number of assets to upload at the same time')
|
||||||
.env('IMMICH_UPLOAD_CONCURRENCY')
|
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||||
.default(4),
|
.default(4),
|
||||||
)
|
)
|
||||||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||||
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||||
.action(async (paths, options) => {
|
.action((paths, options) => upload(paths, program.opts(), options));
|
||||||
options.exclusionPatterns = options.ignore;
|
|
||||||
await new UploadCommand(program.opts()).run(paths, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('server-info')
|
|
||||||
.description('Display server information')
|
|
||||||
.action(async () => {
|
|
||||||
await new ServerInfoCommand(program.opts()).run();
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('login-key')
|
|
||||||
.description('Login using an API key')
|
|
||||||
.argument('url')
|
|
||||||
.argument('key')
|
|
||||||
.action(async (url, key) => {
|
|
||||||
await new LoginCommand(program.opts()).run(url, key);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('logout')
|
|
||||||
.description('Remove stored credentials')
|
|
||||||
.action(async () => {
|
|
||||||
await new LogoutCommand(program.opts()).run();
|
|
||||||
});
|
|
||||||
|
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import {
|
|
||||||
ApiKeyCreateDto,
|
|
||||||
AssetBulkUploadCheckDto,
|
|
||||||
BulkIdsDto,
|
|
||||||
CreateAlbumDto,
|
|
||||||
CreateAssetDto,
|
|
||||||
LoginCredentialDto,
|
|
||||||
SignUpDto,
|
|
||||||
addAssetsToAlbum,
|
|
||||||
checkBulkUpload,
|
|
||||||
createAlbum,
|
|
||||||
createApiKey,
|
|
||||||
getAllAlbums,
|
|
||||||
getAllAssets,
|
|
||||||
getAssetStatistics,
|
|
||||||
getMyUserInfo,
|
|
||||||
getServerVersion,
|
|
||||||
getSupportedMediaTypes,
|
|
||||||
login,
|
|
||||||
pingServer,
|
|
||||||
signUpAdmin,
|
|
||||||
uploadFile,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps the underlying API to abstract away the options and make API calls mockable for testing.
|
|
||||||
*/
|
|
||||||
export class ImmichApi {
|
|
||||||
private readonly options;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public instanceUrl: string,
|
|
||||||
public apiKey: string,
|
|
||||||
) {
|
|
||||||
this.options = {
|
|
||||||
baseUrl: instanceUrl,
|
|
||||||
headers: {
|
|
||||||
'x-api-key': apiKey,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setApiKey(apiKey: string) {
|
|
||||||
this.apiKey = apiKey;
|
|
||||||
if (!this.options.headers) {
|
|
||||||
throw new Error('missing headers');
|
|
||||||
}
|
|
||||||
this.options.headers['x-api-key'] = apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto) {
|
|
||||||
return addAssetsToAlbum({ id, bulkIdsDto }, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkBulkUpload(assetBulkUploadCheckDto: AssetBulkUploadCheckDto) {
|
|
||||||
return checkBulkUpload({ assetBulkUploadCheckDto }, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
createAlbum(createAlbumDto: CreateAlbumDto) {
|
|
||||||
return createAlbum({ createAlbumDto }, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
createApiKey(apiKeyCreateDto: ApiKeyCreateDto, options: { headers: { Authorization: string } }) {
|
|
||||||
return createApiKey({ apiKeyCreateDto }, { ...this.options, ...options });
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllAlbums() {
|
|
||||||
return getAllAlbums({}, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllAssets() {
|
|
||||||
return getAllAssets({}, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAssetStatistics() {
|
|
||||||
return getAssetStatistics({}, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMyUserInfo() {
|
|
||||||
return getMyUserInfo(this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getServerVersion() {
|
|
||||||
return getServerVersion(this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportedMediaTypes() {
|
|
||||||
return getSupportedMediaTypes(this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
login(loginCredentialDto: LoginCredentialDto) {
|
|
||||||
return login({ loginCredentialDto }, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
pingServer() {
|
|
||||||
return pingServer(this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
signUpAdmin(signUpDto: SignUpDto) {
|
|
||||||
return signUpAdmin({ signUpDto }, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadFile(createAssetDto: CreateAssetDto) {
|
|
||||||
return uploadFile({ createAssetDto }, this.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { glob } from 'glob';
|
|
||||||
import * as fs from 'node:fs';
|
|
||||||
|
|
||||||
export class CrawlOptions {
|
|
||||||
pathsToCrawl!: string[];
|
|
||||||
recursive? = false;
|
|
||||||
includeHidden? = false;
|
|
||||||
exclusionPatterns?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CrawlService {
|
|
||||||
private readonly extensions!: string[];
|
|
||||||
|
|
||||||
constructor(image: string[], video: string[]) {
|
|
||||||
this.extensions = [...image, ...video].map((extension) => extension.replace('.', ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
async crawl(options: CrawlOptions): Promise<string[]> {
|
|
||||||
const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
|
|
||||||
|
|
||||||
if (!pathsToCrawl) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const patterns: string[] = [];
|
|
||||||
const crawledFiles: string[] = [];
|
|
||||||
|
|
||||||
for await (const currentPath of pathsToCrawl) {
|
|
||||||
try {
|
|
||||||
const stats = await fs.promises.stat(currentPath);
|
|
||||||
if (stats.isFile() || stats.isSymbolicLink()) {
|
|
||||||
crawledFiles.push(currentPath);
|
|
||||||
} else {
|
|
||||||
patterns.push(currentPath);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
patterns.push(currentPath);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchPattern: string;
|
|
||||||
if (patterns.length === 1) {
|
|
||||||
searchPattern = patterns[0];
|
|
||||||
} else if (patterns.length === 0) {
|
|
||||||
return crawledFiles;
|
|
||||||
} else {
|
|
||||||
searchPattern = '{' + patterns.join(',') + '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recursive) {
|
|
||||||
searchPattern = searchPattern + '/**/';
|
|
||||||
}
|
|
||||||
|
|
||||||
searchPattern = `${searchPattern}/*.{${this.extensions.join(',')}}`;
|
|
||||||
|
|
||||||
const globbedFiles = await glob(searchPattern, {
|
|
||||||
absolute: true,
|
|
||||||
nocase: true,
|
|
||||||
nodir: true,
|
|
||||||
dot: includeHidden,
|
|
||||||
ignore: exclusionPatterns,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...crawledFiles, ...globbedFiles].sort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import yaml from 'yaml';
|
|
||||||
import { SessionService } from './session.service';
|
|
||||||
|
|
||||||
const TEST_CONFIG_DIR = '/tmp/immich/';
|
|
||||||
const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
|
|
||||||
const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
|
|
||||||
const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
|
|
||||||
|
|
||||||
const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
||||||
|
|
||||||
const createTestAuthFile = async (contents: string) => {
|
|
||||||
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
|
||||||
// Create config folder if it doesn't exist
|
|
||||||
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
|
|
||||||
if (!created) {
|
|
||||||
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(TEST_AUTH_FILE, contents);
|
|
||||||
};
|
|
||||||
|
|
||||||
const readTestAuthFile = async (): Promise<string> => {
|
|
||||||
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAuthFile = () => {
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(TEST_AUTH_FILE);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => {
|
|
||||||
return {
|
|
||||||
getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })),
|
|
||||||
pingServer: vi.fn(() => Promise.resolve({ res: 'pong' })),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('./api.service', async (importOriginal) => {
|
|
||||||
const module = await importOriginal<typeof import('./api.service')>();
|
|
||||||
// @ts-expect-error this is only a partial implementation of the return value
|
|
||||||
module.ImmichApi.prototype.getMyUserInfo = mocks.getMyUserInfo;
|
|
||||||
module.ImmichApi.prototype.pingServer = mocks.pingServer;
|
|
||||||
return module;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SessionService', () => {
|
|
||||||
let sessionService: SessionService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
deleteAuthFile();
|
|
||||||
sessionService = new SessionService(TEST_CONFIG_DIR);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
deleteAuthFile();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect to immich', async () => {
|
|
||||||
await createTestAuthFile(
|
|
||||||
JSON.stringify({
|
|
||||||
apiKey: TEST_IMMICH_API_KEY,
|
|
||||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sessionService.connect();
|
|
||||||
expect(mocks.pingServer).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if no auth file exists', async () => {
|
|
||||||
await sessionService.connect().catch((error) => {
|
|
||||||
expect(error.message).toEqual('No auth file exist. Please login first');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if auth file is missing instance URl', async () => {
|
|
||||||
await createTestAuthFile(
|
|
||||||
JSON.stringify({
|
|
||||||
apiKey: TEST_IMMICH_API_KEY,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await sessionService.connect().catch((error) => {
|
|
||||||
expect(error.message).toEqual(`Instance URL missing in auth config file ${TEST_AUTH_FILE}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if auth file is missing api key', async () => {
|
|
||||||
await createTestAuthFile(
|
|
||||||
JSON.stringify({
|
|
||||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(sessionService.connect()).rejects.toThrow(`API key missing in auth config file ${TEST_AUTH_FILE}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create auth file when logged in', async () => {
|
|
||||||
await sessionService.login(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY);
|
|
||||||
|
|
||||||
const data: string = await readTestAuthFile();
|
|
||||||
const authConfig = yaml.parse(data);
|
|
||||||
expect(authConfig.instanceUrl).toBe(TEST_IMMICH_INSTANCE_URL);
|
|
||||||
expect(authConfig.apiKey).toBe(TEST_IMMICH_API_KEY);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete auth file when logging out', async () => {
|
|
||||||
const consoleSpy = spyOnConsole();
|
|
||||||
|
|
||||||
await createTestAuthFile(
|
|
||||||
JSON.stringify({
|
|
||||||
apiKey: TEST_IMMICH_API_KEY,
|
|
||||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await sessionService.logout();
|
|
||||||
|
|
||||||
await fs.promises.access(TEST_AUTH_FILE, fs.constants.F_OK).catch((error) => {
|
|
||||||
expect(error.message).toContain('ENOENT');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(consoleSpy.mock.calls).toEqual([
|
|
||||||
['Logging out...'],
|
|
||||||
[`Removed auth file ${TEST_AUTH_FILE}`],
|
|
||||||
['Successfully logged out'],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { existsSync } from 'node:fs';
|
|
||||||
import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
||||||
import path from 'node:path';
|
|
||||||
import yaml from 'yaml';
|
|
||||||
import { ImmichApi } from './api.service';
|
|
||||||
class LoginError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
|
||||||
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionService {
|
|
||||||
readonly configDirectory!: string;
|
|
||||||
readonly authPath!: string;
|
|
||||||
|
|
||||||
constructor(configDirectory: string) {
|
|
||||||
this.configDirectory = configDirectory;
|
|
||||||
this.authPath = path.join(configDirectory, '/auth.yml');
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect(): Promise<ImmichApi> {
|
|
||||||
let instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
|
||||||
let apiKey = process.env.IMMICH_API_KEY;
|
|
||||||
|
|
||||||
if (!instanceUrl || !apiKey) {
|
|
||||||
await access(this.authPath, constants.F_OK).catch((error) => {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
throw new LoginError('No auth file exist. Please login first');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const data: string = await readFile(this.authPath, 'utf8');
|
|
||||||
const parsedConfig = yaml.parse(data);
|
|
||||||
|
|
||||||
instanceUrl = parsedConfig.instanceUrl;
|
|
||||||
apiKey = parsedConfig.apiKey;
|
|
||||||
|
|
||||||
if (!instanceUrl) {
|
|
||||||
throw new LoginError(`Instance URL missing in auth config file ${this.authPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new LoginError(`API key missing in auth config file ${this.authPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = new ImmichApi(instanceUrl, apiKey);
|
|
||||||
|
|
||||||
const pingResponse = await api.pingServer().catch((error) => {
|
|
||||||
throw new Error(`Failed to connect to server ${instanceUrl}: ${error.message}`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pingResponse.res !== 'pong') {
|
|
||||||
throw new Error(`Could not parse response. Is Immich listening on ${instanceUrl}?`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
|
|
||||||
console.log('Logging in...');
|
|
||||||
|
|
||||||
const api = new ImmichApi(instanceUrl, apiKey);
|
|
||||||
|
|
||||||
// Check if server and api key are valid
|
|
||||||
const userInfo = await api.getMyUserInfo().catch((error) => {
|
|
||||||
throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Logged in as ${userInfo.email}`);
|
|
||||||
|
|
||||||
if (!existsSync(this.configDirectory)) {
|
|
||||||
// Create config folder if it doesn't exist
|
|
||||||
const created = await mkdir(this.configDirectory, { recursive: true });
|
|
||||||
if (!created) {
|
|
||||||
throw new Error(`Failed to create config folder ${this.configDirectory}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 });
|
|
||||||
|
|
||||||
console.log('Wrote auth info to ' + this.authPath);
|
|
||||||
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
|
||||||
console.log('Logging out...');
|
|
||||||
|
|
||||||
if (existsSync(this.authPath)) {
|
|
||||||
await unlink(this.authPath);
|
|
||||||
console.log('Removed auth file ' + this.authPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Successfully logged out');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,31 @@
|
|||||||
import mockfs from 'mock-fs';
|
import mockfs from 'mock-fs';
|
||||||
import { CrawlOptions, CrawlService } from './crawl.service';
|
import { CrawlOptions, crawl } from 'src/utils';
|
||||||
|
|
||||||
interface Test {
|
interface Test {
|
||||||
test: string;
|
test: string;
|
||||||
options: CrawlOptions;
|
options: Omit<CrawlOptions, 'extensions'>;
|
||||||
files: Record<string, boolean>;
|
files: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg',
|
||||||
|
'.png',
|
||||||
|
'.heif',
|
||||||
|
'.heic',
|
||||||
|
'.tif',
|
||||||
|
'.nef',
|
||||||
|
'.webp',
|
||||||
|
'.tiff',
|
||||||
|
'.dng',
|
||||||
|
'.gif',
|
||||||
|
'.mov',
|
||||||
|
'.mp4',
|
||||||
|
'.webm',
|
||||||
|
];
|
||||||
|
|
||||||
const tests: Test[] = [
|
const tests: Test[] = [
|
||||||
{
|
{
|
||||||
test: 'should return empty when crawling an empty path list',
|
test: 'should return empty when crawling an empty path list',
|
||||||
@@ -251,12 +268,7 @@ const tests: Test[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe(CrawlService.name, () => {
|
describe('crawl', () => {
|
||||||
const sut = new CrawlService(
|
|
||||||
['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'],
|
|
||||||
['.mov', '.mp4', '.webm'],
|
|
||||||
);
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockfs.restore();
|
mockfs.restore();
|
||||||
});
|
});
|
||||||
@@ -266,7 +278,7 @@ describe(CrawlService.name, () => {
|
|||||||
it(test, async () => {
|
it(test, async () => {
|
||||||
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
|
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
|
||||||
|
|
||||||
const actual = await sut.crawl(options);
|
const actual = await crawl({ ...options, extensions });
|
||||||
const expected = Object.entries(files)
|
const expected = Object.entries(files)
|
||||||
.filter((entry) => entry[1])
|
.filter((entry) => entry[1])
|
||||||
.map(([file]) => file);
|
.map(([file]) => file);
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { createReadStream } from 'node:fs';
|
||||||
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import yaml from 'yaml';
|
||||||
|
|
||||||
|
export interface BaseOptions {
|
||||||
|
configDirectory: string;
|
||||||
|
key?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthDto = { url: string; key: string };
|
||||||
|
type OldAuthDto = { instanceUrl: string; apiKey: string };
|
||||||
|
|
||||||
|
export const authenticate = async (options: BaseOptions): Promise<AuthDto> => {
|
||||||
|
const { configDirectory: configDir, url, key } = options;
|
||||||
|
|
||||||
|
// provided in command
|
||||||
|
if (url && key) {
|
||||||
|
return connect(url, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to auth file
|
||||||
|
const config = await readAuthFile(configDir);
|
||||||
|
const auth = await connect(config.url, config.key);
|
||||||
|
if (auth.url !== config.url) {
|
||||||
|
await writeAuthFile(configDir, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connect = async (url: string, key: string) => {
|
||||||
|
const wellKnownUrl = new URL('.well-known/immich', url);
|
||||||
|
try {
|
||||||
|
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
||||||
|
const endpoint = new URL(wellKnown.api.endpoint, url).toString();
|
||||||
|
if (endpoint !== url) {
|
||||||
|
console.debug(`Discovered API at ${endpoint}`);
|
||||||
|
}
|
||||||
|
url = endpoint;
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults.baseUrl = url;
|
||||||
|
defaults.headers = { 'x-api-key': key };
|
||||||
|
|
||||||
|
const [error] = await withError(getMyUserInfo());
|
||||||
|
if (isHttpError(error)) {
|
||||||
|
logError(error, 'Failed to connect to server');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url, key };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logError = (error: unknown, message: string) => {
|
||||||
|
if (isHttpError(error)) {
|
||||||
|
console.error(`${message}: ${error.status}`);
|
||||||
|
console.error(JSON.stringify(error.data, undefined, 2));
|
||||||
|
} else {
|
||||||
|
console.error(`${message} - ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAuthFilePath = (dir: string) => join(dir, 'auth.yml');
|
||||||
|
|
||||||
|
export const readAuthFile = async (dir: string) => {
|
||||||
|
try {
|
||||||
|
const data = await readFile(getAuthFilePath(dir));
|
||||||
|
// TODO add class-transform/validation
|
||||||
|
const auth = yaml.parse(data.toString()) as AuthDto | OldAuthDto;
|
||||||
|
const { instanceUrl, apiKey } = auth as OldAuthDto;
|
||||||
|
if (instanceUrl && apiKey) {
|
||||||
|
return { url: instanceUrl, key: apiKey };
|
||||||
|
}
|
||||||
|
return auth as AuthDto;
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
||||||
|
console.log('No auth file exists. Please login first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeAuthFile = async (dir: string, auth: AuthDto) =>
|
||||||
|
writeFile(getAuthFilePath(dir), yaml.stringify(auth), { mode: 0o600 });
|
||||||
|
|
||||||
|
export const withError = async <T>(promise: Promise<T>): Promise<[Error, undefined] | [undefined, T]> => {
|
||||||
|
try {
|
||||||
|
const result = await promise;
|
||||||
|
return [undefined, result];
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
return [error, undefined];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CrawlOptions {
|
||||||
|
pathsToCrawl: string[];
|
||||||
|
recursive?: boolean;
|
||||||
|
includeHidden?: boolean;
|
||||||
|
exclusionPatterns?: string[];
|
||||||
|
extensions: string[];
|
||||||
|
}
|
||||||
|
export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||||
|
const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
|
||||||
|
const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', ''));
|
||||||
|
|
||||||
|
if (pathsToCrawl.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns: string[] = [];
|
||||||
|
const crawledFiles: string[] = [];
|
||||||
|
|
||||||
|
for await (const currentPath of pathsToCrawl) {
|
||||||
|
try {
|
||||||
|
const stats = await stat(currentPath);
|
||||||
|
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||||
|
crawledFiles.push(currentPath);
|
||||||
|
} else {
|
||||||
|
patterns.push(currentPath);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
patterns.push(currentPath);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchPattern: string;
|
||||||
|
if (patterns.length === 1) {
|
||||||
|
searchPattern = patterns[0];
|
||||||
|
} else if (patterns.length === 0) {
|
||||||
|
return crawledFiles;
|
||||||
|
} else {
|
||||||
|
searchPattern = '{' + patterns.join(',') + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recursive) {
|
||||||
|
searchPattern = searchPattern + '/**/';
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`;
|
||||||
|
|
||||||
|
const globbedFiles = await glob(searchPattern, {
|
||||||
|
absolute: true,
|
||||||
|
nocase: true,
|
||||||
|
nodir: true,
|
||||||
|
dot: includeHidden,
|
||||||
|
ignore: exclusionPatterns,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...crawledFiles, ...globbedFiles].sort();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sha1 = (filepath: string) => {
|
||||||
|
const hash = createHash('sha1');
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const rs = createReadStream(filepath);
|
||||||
|
rs.on('error', reject);
|
||||||
|
rs.on('data', (chunk) => hash.update(chunk));
|
||||||
|
rs.on('end', () => resolve(hash.digest('hex')));
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -15,19 +15,7 @@
|
|||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"rootDirs": ["src", "../server/src"],
|
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
|
||||||
"@test": ["../server/test"],
|
|
||||||
"@test/*": ["../server/test/*"],
|
|
||||||
"@test-utils": ["../server/src/test-utils/utils"],
|
|
||||||
"@app/immich": ["../server/src/immich"],
|
|
||||||
"@app/immich/*": ["../server/src/immich/*"],
|
|
||||||
"@app/infra": ["../server/src/infra"],
|
|
||||||
"@app/infra/*": ["../server/src/infra/*"],
|
|
||||||
"@app/domain": ["../server/src/domain"],
|
|
||||||
"@app/domain/*": ["../server/src/domain/*"]
|
|
||||||
},
|
|
||||||
"types": ["vitest/globals"]
|
"types": ["vitest/globals"]
|
||||||
},
|
},
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
@@ -14,4 +15,5 @@ export default defineConfig({
|
|||||||
// bundle everything except for Node built-ins
|
// bundle everything except for Node built-ins
|
||||||
noExternal: /^(?!node:).*$/,
|
noExternal: /^(?!node:).*$/,
|
||||||
},
|
},
|
||||||
|
plugins: [tsconfigPaths()],
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Router_Medium_x5F_Black_00000159464448132936669960000002337362428709113490_"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 266.25"
|
||||||
|
style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#ACCBFA;}
|
||||||
|
.st1{fill:#FA2921;}
|
||||||
|
.st2{fill:#ED79B5;}
|
||||||
|
.st3{fill:#FFB400;}
|
||||||
|
.st4{fill:#1E83F7;}
|
||||||
|
.st5{fill:#18C249;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||||
|
<path class="st0" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||||
|
<path class="st0" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||||
|
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||||
|
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||||
|
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||||
|
<path class="st0" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||||
|
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||||
|
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||||
|
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||||
|
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||||
|
<path class="st2" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||||
|
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||||
|
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||||
|
<path class="st3" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||||
|
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||||
|
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||||
|
<path class="st4" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||||
|
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||||
|
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||||
|
<path class="st5" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||||
|
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||||
|
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Router_Medium_x5F_White" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px" y="0px" viewBox="0 0 792 266.25" style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#4251B0;}
|
||||||
|
.st1{fill:#FA2921;}
|
||||||
|
.st2{fill:#ED79B5;}
|
||||||
|
.st3{fill:#FFB400;}
|
||||||
|
.st4{fill:#1E83F7;}
|
||||||
|
.st5{fill:#18C249;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||||
|
<path class="st0" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||||
|
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||||
|
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||||
|
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||||
|
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||||
|
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||||
|
<path class="st0" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||||
|
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||||
|
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||||
|
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||||
|
<path class="st0" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||||
|
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||||
|
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||||
|
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||||
|
<path class="st0" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||||
|
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||||
|
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||||
|
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||||
|
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||||
|
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||||
|
<path class="st2" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||||
|
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||||
|
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||||
|
<path class="st3" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||||
|
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||||
|
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||||
|
<path class="st4" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||||
|
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||||
|
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||||
|
<path class="st5" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||||
|
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||||
|
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Router_Medium_x5F_Black_00000037681990313894948460000012967653829507626171_"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 792"
|
||||||
|
style="enable-background:new 0 0 792 792;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#ACCBFA;}
|
||||||
|
.st1{fill:#FA2921;}
|
||||||
|
.st2{fill:#ED79B5;}
|
||||||
|
.st3{fill:#FFB400;}
|
||||||
|
.st4{fill:#1E83F7;}
|
||||||
|
.st5{fill:#18C249;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M110.16,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||||
|
C95.71,543.8,102.32,537.4,110.16,537.4z M97.98,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||||
|
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||||
|
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||||
|
<path class="st0" d="M265.44,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||||
|
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||||
|
c-6.81,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.69-5.99-8.05-9.71-15.49-9.71
|
||||||
|
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||||
|
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.81,0,10.74,4.54,11.98,10.53
|
||||||
|
c6.19-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.73-16.73,29.11-16.73
|
||||||
|
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||||
|
C271.43,685.04,265.44,679.26,265.44,671.82z"/>
|
||||||
|
<path class="st0" d="M431.45,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||||
|
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||||
|
c-6.82,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.68-5.99-8.05-9.71-15.49-9.71
|
||||||
|
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||||
|
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.82,0,10.74,4.54,11.98,10.53
|
||||||
|
c6.2-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.72-16.73,29.11-16.73
|
||||||
|
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||||
|
C437.44,685.04,431.45,679.26,431.45,671.82z"/>
|
||||||
|
<path class="st0" d="M491.33,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||||
|
C476.87,543.8,483.48,537.4,491.33,537.4z M479.15,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||||
|
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||||
|
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||||
|
<path class="st0" d="M522.09,634.04c0-29.11,18.17-52.65,48.32-52.65c15.9,0,30.56,7.23,37.17,17.97c2.48,3.92,2.89,6.19,2.89,8.05
|
||||||
|
c0,6.4-4.96,11.77-12.18,11.77c-4.75,0-9.08-2.68-10.94-7.43c-2.89-6.4-8.47-10.12-16.93-10.12c-15.9,0-24.78,14.25-24.78,32.21
|
||||||
|
c0,18.17,9.29,32.21,25.4,32.21c8.67,0,14.87-3.1,17.76-9.5c2.06-4.34,5.99-8.05,11.36-8.05c7.43,0,11.98,5.16,11.98,11.56
|
||||||
|
c0,3.1-1.24,6.81-3.92,10.32c-6.82,9.09-19.62,16.31-37.17,16.31C540.06,686.69,522.09,663.56,522.09,634.04z"/>
|
||||||
|
<path class="st0" d="M690.17,671.82c0-3.51,0.83-9.5,0.83-13.22v-35.3c0-12.6-7.02-21.27-19-21.27c-8.26,0-15.28,3.92-19.82,10.32
|
||||||
|
v46.25c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.78,13.22-13.01,13.22s-13.01-5.78-13.01-13.22c0-3.51,1.03-9.5,1.03-13.22v-99.94
|
||||||
|
c0-3.72-1.03-9.71-1.03-13.22c0-7.43,5.99-13.22,13.01-13.22c7.23,0,13.01,5.78,13.01,13.22c0,3.51-0.83,9.5-0.83,13.22v33.66
|
||||||
|
c6.2-6.81,15.07-10.94,26.43-10.94c21.27,0,36.55,15.9,36.55,38.61v38.61c0,3.72,1.03,9.71,1.03,13.22
|
||||||
|
c0,7.43-5.99,13.22-13.01,13.22C695.95,685.04,690.17,679.26,690.17,671.82z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M376.76,216.42c28.32,25.07,51.15,51.95,65.83,77.27c25.23-45.12,42.08-98.73,42.3-132.88
|
||||||
|
c0-0.24,0-0.46,0-0.66c0-50.53-50.41-70.2-93.82-70.2s-93.82,19.66-93.82,70.2c0,0.69,0,1.62,0,2.73
|
||||||
|
C321.44,173.62,350.14,192.84,376.76,216.42z"/>
|
||||||
|
<path class="st2" d="M222.27,354.21c17.7-19.69,44.85-41.04,75.5-59.08c32.6-19.19,65.21-32.59,93.83-38.73
|
||||||
|
c-35.11-37.94-80.89-70.53-113.31-81.29c-0.23-0.07-0.44-0.14-0.63-0.21c-48.06-15.61-82.34,26.25-95.75,67.54
|
||||||
|
c-13.42,41.29-10.29,95.31,37.77,110.92C220.33,353.58,221.21,353.86,222.27,354.21z"/>
|
||||||
|
<path class="st3" d="M600.73,241.74c-13.42-41.29-47.69-83.15-95.75-67.54c-0.66,0.21-1.54,0.5-2.6,0.84
|
||||||
|
c-2.75,26.34-12.16,59.57-26.36,92.17c-15.09,34.68-33.6,64.69-53.14,86.48c50.7,10.05,106.9,9.52,139.45-0.83
|
||||||
|
c0.23-0.07,0.44-0.14,0.63-0.21C611.02,337.05,614.15,283.03,600.73,241.74z"/>
|
||||||
|
<path class="st4" d="M348.22,394.58c-8.17-36.93-10.84-72.09-7.84-101.2c-46.93,21.67-92.08,55.14-112.33,82.64
|
||||||
|
c-0.14,0.19-0.27,0.37-0.39,0.54c-29.7,40.88-0.48,86.42,34.64,111.94s87.46,39.24,117.16-1.64c0.41-0.56,0.95-1.31,1.6-2.21
|
||||||
|
C367.81,461.72,355.9,429.3,348.22,394.58z"/>
|
||||||
|
<path class="st5" d="M554.19,373.91c-25.9,5.53-60.41,6.84-95.81,3.42c-37.65-3.64-71.91-11.96-98.67-23.82
|
||||||
|
c6.11,51.33,23.99,104.61,43.89,132.37c0.14,0.19,0.27,0.37,0.39,0.54c29.7,40.88,82.04,27.16,117.16,1.64S585.5,417,555.8,376.12
|
||||||
|
C555.39,375.56,554.85,374.81,554.19,373.91z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Router_Medium_x5F_White_00000062189486027058041470000012691761407447023025_"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 792"
|
||||||
|
style="enable-background:new 0 0 792 792;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#4251B0;}
|
||||||
|
.st1{fill:#FA2921;}
|
||||||
|
.st2{fill:#ED79B5;}
|
||||||
|
.st3{fill:#FFB400;}
|
||||||
|
.st4{fill:#1E83F7;}
|
||||||
|
.st5{fill:#18C249;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M110.16,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||||
|
C95.71,543.8,102.32,537.4,110.16,537.4z M97.98,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||||
|
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||||
|
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||||
|
<path class="st0" d="M265.44,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||||
|
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||||
|
c-6.81,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.69-5.99-8.05-9.71-15.49-9.71
|
||||||
|
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||||
|
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.81,0,10.74,4.54,11.98,10.53
|
||||||
|
c6.19-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.73-16.73,29.11-16.73
|
||||||
|
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||||
|
C271.43,685.04,265.44,679.26,265.44,671.82z"/>
|
||||||
|
<path class="st0" d="M431.45,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||||
|
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||||
|
c-6.82,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.68-5.99-8.05-9.71-15.49-9.71
|
||||||
|
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||||
|
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.82,0,10.74,4.54,11.98,10.53
|
||||||
|
c6.2-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.72-16.73,29.11-16.73
|
||||||
|
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||||
|
C437.44,685.04,431.45,679.26,431.45,671.82z"/>
|
||||||
|
<path class="st0" d="M491.33,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||||
|
C476.87,543.8,483.48,537.4,491.33,537.4z M479.15,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||||
|
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||||
|
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||||
|
<path class="st0" d="M522.09,634.04c0-29.11,18.17-52.65,48.32-52.65c15.9,0,30.56,7.23,37.17,17.97c2.48,3.92,2.89,6.19,2.89,8.05
|
||||||
|
c0,6.4-4.96,11.77-12.18,11.77c-4.75,0-9.08-2.68-10.94-7.43c-2.89-6.4-8.47-10.12-16.93-10.12c-15.9,0-24.78,14.25-24.78,32.21
|
||||||
|
c0,18.17,9.29,32.21,25.4,32.21c8.67,0,14.87-3.1,17.76-9.5c2.06-4.34,5.99-8.05,11.36-8.05c7.43,0,11.98,5.16,11.98,11.56
|
||||||
|
c0,3.1-1.24,6.81-3.92,10.32c-6.82,9.09-19.62,16.31-37.17,16.31C540.06,686.69,522.09,663.56,522.09,634.04z"/>
|
||||||
|
<path class="st0" d="M690.17,671.82c0-3.51,0.83-9.5,0.83-13.22v-35.3c0-12.6-7.02-21.27-19-21.27c-8.26,0-15.28,3.92-19.82,10.32
|
||||||
|
v46.25c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.78,13.22-13.01,13.22s-13.01-5.78-13.01-13.22c0-3.51,1.03-9.5,1.03-13.22v-99.94
|
||||||
|
c0-3.72-1.03-9.71-1.03-13.22c0-7.43,5.99-13.22,13.01-13.22c7.23,0,13.01,5.78,13.01,13.22c0,3.51-0.83,9.5-0.83,13.22v33.66
|
||||||
|
c6.2-6.81,15.07-10.94,26.43-10.94c21.27,0,36.55,15.9,36.55,38.61v38.61c0,3.72,1.03,9.71,1.03,13.22
|
||||||
|
c0,7.43-5.99,13.22-13.01,13.22C695.95,685.04,690.17,679.26,690.17,671.82z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M376.76,216.42c28.32,25.07,51.15,51.95,65.83,77.27c25.23-45.12,42.08-98.73,42.3-132.88
|
||||||
|
c0-0.24,0-0.46,0-0.66c0-50.53-50.41-70.2-93.82-70.2s-93.82,19.66-93.82,70.2c0,0.69,0,1.62,0,2.73
|
||||||
|
C321.44,173.62,350.14,192.84,376.76,216.42z"/>
|
||||||
|
<path class="st2" d="M222.27,354.21c17.7-19.69,44.85-41.04,75.5-59.08c32.6-19.19,65.21-32.59,93.83-38.73
|
||||||
|
c-35.11-37.94-80.89-70.53-113.31-81.29c-0.23-0.07-0.44-0.14-0.63-0.21c-48.06-15.61-82.34,26.25-95.75,67.54
|
||||||
|
c-13.42,41.29-10.29,95.31,37.77,110.92C220.33,353.58,221.21,353.86,222.27,354.21z"/>
|
||||||
|
<path class="st3" d="M600.73,241.74c-13.42-41.29-47.69-83.15-95.75-67.54c-0.66,0.21-1.54,0.5-2.6,0.84
|
||||||
|
c-2.75,26.34-12.16,59.57-26.36,92.17c-15.09,34.68-33.6,64.69-53.14,86.48c50.7,10.05,106.9,9.52,139.45-0.83
|
||||||
|
c0.23-0.07,0.44-0.14,0.63-0.21C611.02,337.05,614.15,283.03,600.73,241.74z"/>
|
||||||
|
<path class="st4" d="M348.22,394.58c-8.17-36.93-10.84-72.09-7.84-101.2c-46.93,21.67-92.08,55.14-112.33,82.64
|
||||||
|
c-0.14,0.19-0.27,0.37-0.39,0.54c-29.7,40.88-0.48,86.42,34.64,111.94s87.46,39.24,117.16-1.64c0.41-0.56,0.95-1.31,1.6-2.21
|
||||||
|
C367.81,461.72,355.9,429.3,348.22,394.58z"/>
|
||||||
|
<path class="st5" d="M554.19,373.91c-25.9,5.53-60.41,6.84-95.81,3.42c-37.65-3.64-71.91-11.96-98.67-23.82
|
||||||
|
c6.11,51.33,23.99,104.61,43.89,132.37c0.14,0.19,0.27,0.37,0.39,0.54c29.7,40.88,82.04,27.16,117.16,1.64S585.5,417,555.8,376.12
|
||||||
|
C555.39,375.56,554.85,374.81,554.19,373.91z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -1,98 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<svg version="1.1" id="svg2781" xmlns:svg="http://www.w3.org/2000/svg"
|
<svg version="1.1" id="Flower" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 564.2 553.5"
|
viewBox="0 0 792 792" style="enable-background:new 0 0 792 792;" xml:space="preserve">
|
||||||
style="enable-background:new 0 0 564.2 553.5;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.st0{fill:#4081EF;stroke:#512D8C;stroke-miterlimit:10;}
|
.st0{fill:#FA2921;}
|
||||||
.st1{fill:#31A452;stroke:#512D8C;stroke-miterlimit:10;}
|
.st1{fill:#ED79B5;}
|
||||||
.st2{fill:#DE7FB3;stroke:#512D8C;stroke-miterlimit:10;}
|
.st2{fill:#FFB400;}
|
||||||
.st3{fill:#FFB800;stroke:#512D8C;stroke-miterlimit:10;}
|
.st3{fill:#1E83F7;}
|
||||||
.st4{fill:#E64132;stroke:#512D8C;stroke-miterlimit:10;}
|
.st4{fill:#18C249;}
|
||||||
.st5{fill:#F2F5FB;stroke:#512D8C;stroke-miterlimit:10;}
|
|
||||||
</style>
|
</style>
|
||||||
<path class="st0" d="M210.5,549.6c-2.2-0.2-5.5-1-9.7-2.2c-52.4-15.7-99-46.5-133.8-88.5c-8.8-10.7-17.2-22.4-19.4-27.5
|
<g id="Flower_00000077325900055813483940000000694823054982625702_">
|
||||||
c-8.1-18.1-6.3-38.7,4.8-55.4c5-7.5,13.2-15,20.5-18.7c1.2-0.6,54.1-20,55.8-20.4c0.5-0.1,0.5,0.2-0.3,2.1c-0.7,1.7-1,3.1-1.1,5.5
|
<path class="st0" d="M375.48,267.63c38.64,34.21,69.78,70.87,89.82,105.42c34.42-61.56,57.42-134.71,57.71-181.3
|
||||||
l-0.1,3.2l2.8,5.8c8.7,17.9,19.2,32.7,33.2,46.4c6.3,6.2,7.8,7.6,13.8,12.3c22.7,18.1,52,30.7,79.9,34.3c2.5,0.3,5,0.8,5.7,1
|
c0-0.33,0-0.63,0-0.91c0-68.94-68.77-95.77-128.01-95.77s-128.01,26.83-128.01,95.77c0,0.94,0,2.2,0,3.72
|
||||||
c2.8,0.9,7.7-0.8,11-3.7l1.8-1.6l-0.2,4.8c-0.1,2.7-0.6,15.4-1,28.3c-0.6,20.3-0.8,24-1.5,27.5c-3.9,20.7-18.6,37.5-38.4,44.1
|
C300.01,209.24,339.15,235.47,375.48,267.63z"/>
|
||||||
c-4.6,1.5-8,2.2-13.1,2.7C216.6,550.1,215.3,550,210.5,549.6z"/>
|
<path class="st1" d="M164.7,455.63c24.15-26.87,61.2-55.99,103.01-80.61c44.48-26.18,88.97-44.47,128.02-52.84
|
||||||
<path class="st1" d="M339.8,549.4c-4-0.4-9.4-1.6-13.2-2.9c-3.4-1.2-10-4.4-12.5-6.1c-10.9-7.4-19-17.9-23.1-30
|
c-47.91-51.76-110.37-96.24-154.6-110.91c-0.31-0.1-0.6-0.19-0.86-0.28c-65.57-21.3-112.34,35.81-130.64,92.15
|
||||||
c-2.2-6.7-2.3-7.5-3.3-36.9c-0.5-14.9-0.9-27.9-0.9-28.9l0-1.9l2.3,1.8c2.6,2,6.6,3.4,8.5,3.1c0.6-0.1,3-0.5,5.3-0.8
|
c-18.3,56.34-14.04,130.04,51.53,151.34C162.05,454.77,163.25,455.16,164.7,455.63z"/>
|
||||||
c37.7-5.3,71.2-22.2,97.4-49.1c12.2-12.5,21.4-25.5,29.9-42.4l3.5-7l0-3.6c0-3.1-0.1-3.8-1-5.7c-0.5-1.2-0.9-2.1-0.9-2.2
|
<path class="st2" d="M681.07,302.19c-18.3-56.34-65.07-113.45-130.64-92.15c-0.9,0.29-2.1,0.68-3.54,1.15
|
||||||
c0.2-0.2,55.3,20.1,56.9,20.9c2.6,1.3,6.6,4.1,9.9,7c9.2,7.7,16.1,19.4,18.8,31.8c0.7,3.1,0.8,4.8,0.8,11.3c0,8.6-0.5,11.7-2.9,18.7
|
c-3.75,35.93-16.6,81.27-35.96,125.76c-20.59,47.32-45.84,88.27-72.51,118c69.18,13.72,145.86,12.98,190.26-1.14
|
||||||
c-1.7,5-2.9,7.2-7.1,13.1c-7.6,11-15.3,20.5-25.2,31.2c-32.8,35.4-76.5,62.5-123.4,76.3C351.6,549.6,347.2,550.1,339.8,549.4z"/>
|
c0.31-0.1,0.6-0.2,0.86-0.28C695.11,432.22,699.37,358.52,681.07,302.19z"/>
|
||||||
<path class="st2" d="M255.6,438c-25.9-4.2-50.7-14.9-71.7-31c-5.2-4-8.7-7.1-14.1-12.4c-12.7-12.5-21.9-24.9-30.5-41.4
|
<path class="st3" d="M336.54,510.71c-11.15-50.39-14.8-98.36-10.7-138.08c-64.03,29.57-125.63,75.23-153.26,112.76
|
||||||
c-2.3-4.4-2.4-4.7-2.4-7.1c0-8.8,8.5-15.2,16.9-12.7c5.6,1.7,9.6,6.8,9.7,12.2c0,2.6-0.8,4.6-2.6,6.2c-1.2,1.1-3.2,1.9-4.6,1.9
|
c-0.19,0.26-0.37,0.51-0.53,0.73c-40.52,55.78-0.66,117.91,47.27,152.72c47.92,34.82,119.33,53.54,159.86-2.24
|
||||||
c-1.2,0-3.3-0.8-4.3-1.6c-2.1-1.8-2-1,0.4,3.2c19.3,33.8,52.3,59.1,90,69.1c5.7,1.5,11.5,2.7,11.8,2.4c0.1-0.1-0.4-0.8-1.3-1.6
|
c0.56-0.76,1.3-1.78,2.19-3.01C363.28,602.32,347.02,558.08,336.54,510.71z"/>
|
||||||
c-5.1-4.5-2.3-11.7,5-12.8c5.4-0.8,11.4,2.7,13.9,8c0.8,1.7,1,2.5,1,5.3s-0.1,3.5-1,5.3c-2,4.3-6.8,7.9-10.3,7.8
|
<path class="st4" d="M617.57,482.52c-35.33,7.54-82.42,9.33-130.72,4.66c-51.37-4.96-98.11-16.32-134.63-32.5
|
||||||
C260.6,438.7,257.9,438.3,255.6,438z"/>
|
c8.33,70.03,32.73,142.73,59.88,180.6c0.19,0.26,0.37,0.51,0.53,0.73c40.52,55.78,111.93,37.06,159.86,2.24
|
||||||
<path class="st0" d="M297.6,438.2c-3.4-1.3-6.4-4.3-7.8-8.1c-1.1-2.9-0.9-7.3,0.5-10.2c2.6-5.3,8.7-8.5,14.4-7.5
|
c47.92-34.82,87.79-96.95,47.27-152.72C619.2,484.77,618.46,483.75,617.57,482.52z"/>
|
||||||
c2.9,0.5,4.7,1.9,6,4.3c0.8,1.6,1,2.2,0.8,3.6c-0.3,2.2-0.9,3.3-2.7,4.8c-0.8,0.7-1.4,1.4-1.3,1.5c0.5,0.5,13.4-2.7,21.3-5.4
|
</g>
|
||||||
c33.6-11.3,62.5-35.1,80.4-66.1c2.5-4.4,2.6-5,0.5-3.2c-2.8,2.4-7,1.9-9.6-1c-4-4.6-0.7-13.8,6.1-16.9c2-0.9,2.7-1,5.5-1
|
|
||||||
c2.9,0,3.5,0.1,5.6,1.1c4.4,2.1,7.4,6.4,7.8,11c0.2,2.2,0.1,2.3-2.2,6.9c-23,45.9-67,78.1-117.2,85.9
|
|
||||||
C300.2,438.8,299.4,438.9,297.6,438.2z"/>
|
|
||||||
<path class="st1" d="M211.1,398.5c-4.7-0.9-8.7-2.7-12.9-5.9c-10.8-8.1-13.5-22.3-6.6-33.7c0.7-1.2,1.1-2.2,1-2.4
|
|
||||||
c-0.2-0.2-1.2-0.6-2.3-1.1c-7.6-3-13-10.6-13.5-19.1c-0.5-7.4,3.1-15,9-19.4c1-0.7,2.2-1.5,2.6-1.8c0.8-0.4,68.9-22.7,69.4-22.7
|
|
||||||
c0.2,0,0.7,0.7,1.2,1.5c0.5,0.8,1.6,2.3,2.4,3.3c1.2,1.4,1.5,1.9,1.2,2.3c-0.2,0.3-6.9,9.5-14.8,20.5
|
|
||||||
c-15.9,21.9-15.5,21.3-13.4,23.4c1.3,1.3,2.9,1.4,4.4,0.3c0.6-0.4,7.5-9.7,15.5-20.7c11.2-15.4,14.6-19.9,15-19.7
|
|
||||||
c0.9,0.4,5.5,1.9,6.6,2.1l1,0.2l0,35.3c0,39.7,0,38.8-2.5,44c-2.6,5.3-7.2,9.3-12.7,11.2c-3.7,1.3-6.8,1.6-10.2,1
|
|
||||||
c-5.5-0.9-9.8-3.2-13.7-7.4l-2.2-2.4l-0.6,0.9c-3,4.3-8.6,8.1-14,9.5C218.2,398.6,213.2,398.9,211.1,398.5z"/>
|
|
||||||
<path class="st3" d="M342.9,398.5c-5.5-0.9-9.9-3.2-14.3-7.6l-3.2-3.2l-0.7,1c-2.3,3.3-6.8,6.5-11.1,7.9c-3.7,1.2-9.2,1.4-12.6,0.3
|
|
||||||
c-7.1-2.1-12.7-7.4-15.2-14.3l-0.9-2.6v-37.1v-37.1l1.8-0.4c1-0.2,2.7-0.8,3.9-1.2c1.1-0.5,2.1-0.8,2.2-0.7c0.1,0.1,6.5,9,14.4,19.9
|
|
||||||
c7.8,10.9,14.7,20.1,15.2,20.5c2.2,1.9,5.4,0.4,5.4-2.6c0-1.4-1-2.9-13.8-20.5c-7.6-10.5-14.2-19.6-14.7-20.4l-0.9-1.3l1.4-1.7
|
|
||||||
c0.8-0.9,1.9-2.5,2.5-3.4l1-1.6l34.4,11.2c18.9,6.2,35.1,11.6,35.9,12.1c6.8,4,11.1,11.3,11.1,19.1c0,4.1-0.5,6.4-2.4,10.2
|
|
||||||
c-2,4.1-5.5,7.6-9.6,9.7c-1.6,0.8-3.2,1.5-3.4,1.5c-1,0-0.9,0.7,0.3,2.6c2.8,4.3,4,8.5,3.9,13.7c0,8.1-3.7,15.2-10.6,20.3
|
|
||||||
C356.4,397.6,349.5,399.5,342.9,398.5z"/>
|
|
||||||
<path class="st2" d="M53.9,341.9c-0.5-0.1-2.3-0.4-3.9-0.7c-15.6-2.6-30.4-12.6-38.8-26.2c-3.5-5.7-6.4-13.2-7.8-19.9
|
|
||||||
c-1.2-6.1-0.8-28.1,0.8-43.1c4.5-43,19-84.3,42.2-120.7c6.5-10.2,14.9-21.5,18.2-24.6c17.8-16.6,43.1-20.5,64.8-10
|
|
||||||
c4.3,2.1,8.8,5.1,12.7,8.6c2.8,2.4,5.8,6.1,20.9,25.5c9.7,12.5,17.8,22.8,17.9,23c0.2,0.2-0.9,0.4-3.2,0.4c-2.5,0-4.1,0.2-5.7,0.7
|
|
||||||
c-2.1,0.7-2.6,1.1-7.9,6.3c-8.2,8.1-14.4,15.3-20.3,23.9c-15.5,22.2-25.4,47.7-28.8,74.8c-2.2,16.9-1.6,37.5,1.6,52.3
|
|
||||||
c0.3,1.4,0.5,2.8,0.4,3c-0.1,0.2,0.2,1.3,0.8,2.4c1.1,2.4,4.3,5.7,6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2-13.1,3.8-27.6,8
|
|
||||||
c-16.4,4.7-27.7,7.8-29.8,8.1C64.1,342.1,56.1,342.3,53.9,341.9z"/>
|
|
||||||
<path class="st3" d="M494.7,341.7c-2.1-0.3-33.8-9.1-56.5-15.8l-2.5-0.7l1.6-0.8c3.4-1.7,7.2-6.6,7.3-9.6c0-0.7,0.4-3.3,0.8-5.8
|
|
||||||
c3.9-22.7,3.1-46.1-2.5-68.4c-6.4-25.5-18.6-49.2-35.8-69.1c-4.6-5.3-14.8-15.4-16.4-16.1c-2.4-1.1-5.1-1.6-8-1.4l-2.7,0.2l1.2-1.5
|
|
||||||
c0.7-0.8,8.5-10.8,17.5-22.3c8.9-11.5,17.2-21.8,18.5-23.1c2.6-2.7,7-6.2,10.3-8.2c19.3-11.6,43-11.1,61.6,1.2
|
|
||||||
c5.4,3.6,8.2,6.2,12.3,11.7c26.4,34.5,44,73.7,52.3,116.2c3.4,17.6,4.9,33.3,5,52.4c0,13-0.2,14.8-2.5,21.8
|
|
||||||
C547.8,328.6,521.7,345.2,494.7,341.7z"/>
|
|
||||||
<path class="st4" d="M133.9,318.5c-2-0.5-4.6-1.9-6-3.3c-2.5-2.4-3.1-3.5-3.7-7.3c-4.4-27.3-2.2-54,6.7-79.3
|
|
||||||
c5.3-15.1,13.5-30.5,23-43.1c5.8-7.8,16.6-19.5,19-20.7c4.7-2.4,11.3-1.2,15.2,2.7c5.4,5.4,5.2,13.9-0.3,19.1
|
|
||||||
c-4.3,4-9.4,4.4-12.6,0.9c-1.7-1.9-2.2-3.9-1.7-6.4c0.2-1.1,0.3-2,0.2-2.2c-0.3-0.3-3.6,3.3-8.3,9.1c-17.6,21.8-28.5,48-31.9,76.5
|
|
||||||
c-1.1,9.3-1,26.4,0.1,34.6c0.3,1.8,0.8,1.9,1.4,0.1c0.9-2.6,4-4.7,6.8-4.7c3,0,5.9,2.2,7.5,5.7c0.6,1.3,0.8,2.3,0.8,5.2
|
|
||||||
c0,3.3-0.1,3.8-1.1,5.7c-1.4,2.7-4.6,5.7-7.1,6.6C139.4,318.6,135.8,318.9,133.9,318.5z"/>
|
|
||||||
<path class="st1" d="M422.6,318.5c-3.7-0.6-7.7-3.6-9.4-7.1c-3.8-7.5,0.1-16.9,6.9-16.9c3.1,0,5.8,2,6.9,5.2
|
|
||||||
c0.4,1.2,0.5,1.3,0.7,0.7c1.3-3.7,1.7-26.4,0.6-35.7c-3.6-29.6-14.5-55.3-33-77.9c-5.5-6.7-8.4-9.4-7.1-6.6c0.7,1.4,0.5,4.3-0.3,5.9
|
|
||||||
c-0.9,1.7-3.2,3.5-5,3.8c-3.2,0.6-7.9-1.6-10.2-4.8c-6.5-8.8-0.5-21.2,10.4-21.4c4.6-0.1,5.2,0.3,11.2,6.4
|
|
||||||
c12.1,12.3,21.1,24.9,28.8,40.3c13.2,26.3,18.6,54.9,16.1,84.5c-0.5,5.6-2,15.7-2.6,17.1c-1.3,2.8-4.8,5.5-8.4,6.5
|
|
||||||
C425.9,318.9,425.1,318.9,422.6,318.5z"/>
|
|
||||||
<path class="st0" d="M178.2,307.2c-6-1.3-12.2-6.2-14.9-11.7c-3.4-7-3.1-15.1,0.9-21.6c0.7-1.2,1.2-2.3,1.1-2.4
|
|
||||||
c-0.1-0.1-1.1-0.6-2.1-1c-3.9-1.5-8.1-4.8-10.7-8.3c-4.6-6.2-6.1-14.6-3.9-22.1c2.9-10.3,9.4-16.8,19.1-19.3c2.8-0.7,9-0.8,11.7,0
|
|
||||||
c1.1,0.3,2.2,0.5,2.4,0.5c0.2,0,0.3-0.7,0.3-1.5c0-2.9,0.8-5.8,2.4-9.2c5.2-10.8,18.1-15.5,29-10.5c2.7,1.2,6.2,3.8,7.8,5.8
|
|
||||||
c0.7,0.8,10.3,14,21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1-1.9,2.6-2.5,3.5c-0.6,1-1.2,1.7-1.5,1.6c-4.5-1.7-46.7-15-47.7-15
|
|
||||||
c-1.9,0-3.1,1.3-3.1,3.2c0,1,0.2,1.7,0.8,2.3c0.6,0.6,7.8,3.1,24.5,8.5l23.7,7.7l-0.1,4.3l-0.1,4.3L223,295.9
|
|
||||||
c-18,5.9-33.9,10.9-35.2,11.2C184.7,307.8,181.2,307.8,178.2,307.2z"/>
|
|
||||||
<path class="st4" d="M372.5,306.8c-1.8-0.5-17.5-5.6-35-11.3l-31.8-10.4l1-4.3v-4.3l22.6-7.7c15-4.9,24-8,24.6-8.5
|
|
||||||
c0.7-0.6,0.9-1.1,0.9-2.2c0-2-1.2-3.3-3.1-3.3c-0.9,0-10.5,2.9-24.7,7.5c-12.8,4.1-23.4,7.5-23.6,7.5c-0.1,0-0.7-0.8-1.3-1.9
|
|
||||||
c-0.6-1-1.6-2.5-2.2-3.2c-0.7-0.7-1.2-1.5-1.2-1.6c0-0.2,9.6-13.5,21.4-29.6c18.9-26,21.6-29.6,23.6-31.1c5.7-4.4,13.1-5.8,19.7-3.9
|
|
||||||
c9,2.7,16.1,11.6,16.1,20.3c0,2.3-0.1,2.3,3.1,1.5c4.7-1.1,11.5-0.5,16,1.5c4.6,2,9,6,11.5,10.2c2.1,3.6,3.9,9.4,4.2,13.2
|
|
||||||
c0.3,5.2-1.1,10.7-4,15.3c-2.6,4.1-7.8,8.3-12.1,9.8c-0.9,0.3-1.7,0.8-1.7,1c0,0.2,0.4,1,0.9,1.7c2.4,3.6,3.6,7.7,3.5,12.7
|
|
||||||
c0,5.8-2.1,10.7-6.4,15.1c-4,4.1-8.9,6.3-14.9,6.5C376.3,307.7,375.3,307.6,372.5,306.8z"/>
|
|
||||||
<path class="st5" d="M276.2,298.9c-6.1-1.6-11.4-6.8-13.2-12.9c-0.7-2.4-0.7-7.5,0-9.9c1.7-5.8,6.6-10.8,12.3-12.5
|
|
||||||
c2.7-0.8,7.2-0.9,10-0.2c6.2,1.6,11.6,7.1,13.2,13.3c1.6,6-0.3,12.6-5,17.3C288.9,298.6,282.2,300.5,276.2,298.9z"/>
|
|
||||||
<path class="st2" d="M248.3,229.8c-13.3-18.3-21.2-29.6-22-31.1c-1.4-3-1.9-5.5-1.9-9.4c0-14.1,13.1-24.4,27.1-21.4
|
|
||||||
c1.4,0.3,2.6,0.5,2.7,0.5s0.3-1.3,0.4-2.8c0.8-10.7,8.4-19.6,18.9-22.4c3.9-1,10.6-1,14.5,0c8.9,2.3,15.9,9.3,18.2,18.2
|
|
||||||
c0.4,1.5,0.7,3.7,0.7,4.9c0,1.2,0.1,2.1,0.3,2.1s1.5-0.3,3-0.6c7.4-1.6,15.2,0.7,20.5,6c4.3,4.3,6.6,9.6,6.6,15.6
|
|
||||||
c0,4-0.6,6.5-2.4,10c-0.6,1.2-10.4,15-21.7,30.7c-17.8,24.5-20.8,28.5-21.4,28.3c-0.4-0.1-1.9-0.6-3.4-1.1c-1.5-0.5-2.9-0.9-3.3-0.9
|
|
||||||
c-0.7,0-0.7-0.8-0.3-25.5v-25.5l-1.4-0.9c-1-1.1-2.5-1.5-3.8-0.9c-2,0.8-2-0.5-1.8,27.2v25.8h-1.2c-0.5-0.2-2.4,0.3-4,0.9
|
|
||||||
s-3.1,1.1-3.2,1.1C269.2,258.5,259.8,245.6,248.3,229.8z"/>
|
|
||||||
<path class="st3" d="M210.9,164.8c-4.1-0.9-7.7-3.6-9.6-7.4c-1.4-2.8-1.7-7.3-0.5-10.3c1.7-4.5,3.9-6.1,15.6-11.2
|
|
||||||
c15.8-7,31.4-11.1,49.2-12.9c7.3-0.8,23.2-0.8,30.6,0c17.4,1.8,33.3,6,49.1,13c7.3,3.2,12.5,6.1,13.6,7.5c4.3,5.6,3.8,12.7-1.1,17.6
|
|
||||||
c-5.1,5.1-12.9,5.4-18.1,0.7c-2-1.8-3-3.5-3.4-5.6c-0.7-4,2.9-8.1,7.3-8.2c1.4,0,1.5-0.1,1.1-0.5c-0.3-0.3-2.2-1.2-4.3-2.1
|
|
||||||
c-33.2-14.5-70.5-16.4-105-5.4c-7.5,2.4-19,7.2-18.6,7.7c0.1,0.2,0.8,0.3,1.6,0.3c5.6,0,9.1,6.2,6.1,10.8
|
|
||||||
C221.6,163.3,215.9,165.9,210.9,164.8z"/>
|
|
||||||
<path class="st4" d="M174.7,123.4c-8.9-13.1-16.8-25.1-17.5-26.6c-1.6-3.3-3.6-9.2-4.4-13c-2.6-12.5-0.9-25.8,5-37.5
|
|
||||||
c4.2-8.3,11.2-16.3,18.6-21.3c5-3.4,6.1-3.9,12.8-6.3c23.1-8.2,47.2-13.1,73.4-15c7.5-0.6,28.5-0.6,36.3,0
|
|
||||||
c25.5,1.8,50.6,6.9,73,14.8c6.4,2.2,8.2,3.1,13.1,6.5c9.8,6.6,18.1,17.5,22,29.2c2.2,6.5,2.7,10,2.7,17.9c0,7.9-0.5,11.3-2.7,17.9
|
|
||||||
c-2.3,6.8-3.7,9.1-20.3,33.6l-16.1,23.8l-0.4-2.2c-0.2-1.2-0.9-3-1.4-4c-1-1.8-4.4-5.6-4.7-5.2c-0.1,0.1-1.2-0.4-2.4-1.1
|
|
||||||
c-9.1-5.2-21.9-10.5-33.2-13.9c-37-11-77.2-8.8-113,6.1c-4.9,2.1-17.7,8.4-19.2,9.5c-2.2,1.6-5.1,6.8-5.1,9c0,0.4-0.1,1-0.3,1.2
|
|
||||||
C191,147,184.7,138,174.7,123.4z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 540 KiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 570 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 206 KiB |
@@ -2,8 +2,6 @@
|
|||||||
# - https://immich.app/docs/developer/setup
|
# - https://immich.app/docs/developer/setup
|
||||||
# - https://immich.app/docs/developer/troubleshooting
|
# - https://immich.app/docs/developer/troubleshooting
|
||||||
|
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
name: immich-dev
|
name: immich-dev
|
||||||
|
|
||||||
x-server-build: &server-common
|
x-server-build: &server-common
|
||||||
@@ -30,7 +28,7 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
|
command: ['/usr/src/app/bin/immich-dev', 'immich']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001
|
||||||
@@ -41,7 +39,7 @@ services:
|
|||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
|
command: ['/usr/src/app/bin/immich-dev', 'microservices']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -57,7 +55,7 @@ services:
|
|||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
command: [ "/usr/src/app/bin/immich-web" ]
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
@@ -99,7 +97,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -115,5 +113,28 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
|
# set IMMICH_METRICS=true in .env to enable metrics
|
||||||
|
# immich-prometheus:
|
||||||
|
# container_name: immich_prometheus
|
||||||
|
# ports:
|
||||||
|
# - 9090:9090
|
||||||
|
# image: prom/prometheus
|
||||||
|
# volumes:
|
||||||
|
# - ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
# - prometheus-data:/prometheus
|
||||||
|
|
||||||
|
# first login uses admin/admin
|
||||||
|
# add data source for http://immich-prometheus:9090 to get started
|
||||||
|
# immich-grafana:
|
||||||
|
# container_name: immich_grafana
|
||||||
|
# command: ['./run.sh', '-disable-reporting']
|
||||||
|
# ports:
|
||||||
|
# - 3000:3000
|
||||||
|
# image: grafana/grafana:10.3.3-ubuntu
|
||||||
|
# volumes:
|
||||||
|
# - grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
prometheus-data:
|
||||||
|
grafana-data:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
name: immich-prod
|
name: immich-prod
|
||||||
|
|
||||||
x-server-build: &server-common
|
x-server-build: &server-common
|
||||||
@@ -17,7 +15,7 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: [ "start.sh", "immich" ]
|
command: ['start.sh', 'immich']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
@@ -27,7 +25,7 @@ services:
|
|||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
command: [ "start.sh", "microservices" ]
|
command: ['start.sh', 'microservices']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -56,7 +54,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@@ -73,5 +71,28 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
|
# set IMMICH_METRICS=true in .env to enable metrics
|
||||||
|
immich-prometheus:
|
||||||
|
container_name: immich_prometheus
|
||||||
|
ports:
|
||||||
|
- 9090:9090
|
||||||
|
image: prom/prometheus@sha256:4f6c47e39a9064028766e8c95890ed15690c30f00c4ba14e7ce6ae1ded0295b1
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
- prometheus-data:/prometheus
|
||||||
|
|
||||||
|
# first login uses admin/admin
|
||||||
|
# add data source for http://immich-prometheus:9090 to get started
|
||||||
|
immich-grafana:
|
||||||
|
container_name: immich_grafana
|
||||||
|
command: ['./run.sh', '-disable-reporting']
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c
|
||||||
|
volumes:
|
||||||
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
prometheus-data:
|
||||||
|
grafana-data:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||||
#
|
#
|
||||||
@@ -14,7 +12,7 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
command: [ "start.sh", "immich" ]
|
command: ['start.sh', 'immich']
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -33,7 +31,7 @@ services:
|
|||||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
command: [ "start.sh", "microservices" ]
|
command: ['start.sh', 'microservices']
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -60,20 +58,19 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
|
||||||
model-cache:
|
model-cache:
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ DB_PASSWORD=postgres
|
|||||||
DB_HOSTNAME=immich_postgres
|
DB_HOSTNAME=immich_postgres
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
|
DB_DATA_LOCATION=./postgres
|
||||||
|
|
||||||
REDIS_HOSTNAME=immich_redis
|
REDIS_HOSTNAME=immich_redis
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ services:
|
|||||||
count: 1
|
count: 1
|
||||||
capabilities:
|
capabilities:
|
||||||
- gpu
|
- gpu
|
||||||
- compute
|
|
||||||
- video
|
|
||||||
|
|
||||||
openvino:
|
openvino:
|
||||||
device_cgroup_rules:
|
device_cgroup_rules:
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ services:
|
|||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
- /dev/dma_heap:/dev/dma_heap
|
- /dev/dma_heap:/dev/dma_heap
|
||||||
- /dev/mpp_service:/dev/mpp_service
|
- /dev/mpp_service:/dev/mpp_service
|
||||||
|
#- /dev/mali0:/dev/mali0 # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||||
|
volumes:
|
||||||
|
#- /etc/OpenCL:/etc/OpenCL:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||||
|
#- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||||
|
|
||||||
vaapi:
|
vaapi:
|
||||||
devices:
|
devices:
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: immich_server
|
||||||
|
static_configs:
|
||||||
|
- targets: ['immich-server:8081']
|
||||||
|
|
||||||
|
- job_name: immich_microservices
|
||||||
|
static_configs:
|
||||||
|
- targets: ['immich-microservices:8081']
|
||||||
@@ -10,8 +10,8 @@ Hello everyone, it is my pleasure to deliver the new release of Immich to you. T
|
|||||||
|
|
||||||
Some notable features are:
|
Some notable features are:
|
||||||
|
|
||||||
- [OAuth integration](#livephoto-ios-support-)
|
- OAuth integration
|
||||||
- [LivePhoto support on iOS](#oauth-integration-)
|
- LivePhoto support on iOS
|
||||||
- User config system
|
- User config system
|
||||||
|
|
||||||
<!--truncate-->
|
<!--truncate-->
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ No. Our golden rule is that the original assets should always be untouched, so w
|
|||||||
|
|
||||||
### How can I move all data (photos, persons, albums) from one user to another?
|
### How can I move all data (photos, persons, albums) from one user to another?
|
||||||
|
|
||||||
This is not officially supported, but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the psql command), or you can add for example an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file, so that you can use a web-interface.
|
This is not officially supported, but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add for example an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file, so that you can use a web-interface.
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
This is an advanced operation. If you can't do it with the steps described here, this is not for you.
|
This is an advanced operation. If you can't do it with the steps described here, this is not for you.
|
||||||
@@ -253,8 +253,19 @@ The initial backup is the most intensive due to the number of jobs running. The
|
|||||||
|
|
||||||
### Can I limit the amount of CPU and RAM usage?
|
### Can I limit the amount of CPU and RAM usage?
|
||||||
|
|
||||||
By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows.
|
By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources.
|
||||||
You can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit) to learn how to limit this.
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
# Number of CPU threads
|
||||||
|
cpus: '1.00'
|
||||||
|
# Gigabytes of memory
|
||||||
|
memory: '1G'
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit).
|
||||||
|
|
||||||
### How can I boost machine learning speed?
|
### How can I boost machine learning speed?
|
||||||
|
|
||||||
@@ -288,10 +299,25 @@ Immich components are typically deployed using docker. To see logs for deployed
|
|||||||
### How can I run Immich as a non-root user?
|
### How can I run Immich as a non-root user?
|
||||||
|
|
||||||
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
|
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
|
||||||
You may need to add an additional volume to `immich-microservices` that mounts internally to `/usr/src/app/.reverse-geocoding-dump`.
|
You may need to add mount points or docker volumes for the following internal container paths:
|
||||||
|
|
||||||
|
- `immich-machine-learning:/.config`
|
||||||
|
- `immich-machine-learning:/.cache`
|
||||||
|
- `redis:/data`
|
||||||
|
|
||||||
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
|
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
|
||||||
|
|
||||||
|
For a further hardened system, you can add the following block to every container except for `immich_postgres`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security_opt:
|
||||||
|
# Prevent escalation of privileges after container is started
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
# Prevent access to raw network traffic
|
||||||
|
- NET_RAW
|
||||||
|
```
|
||||||
|
|
||||||
### How can I **purge** data from Immich?
|
### How can I **purge** data from Immich?
|
||||||
|
|
||||||
Data for Immich comes in two forms:
|
Data for Immich comes in two forms:
|
||||||
@@ -311,7 +337,7 @@ docker compose down -v
|
|||||||
|
|
||||||
:::note Portainer
|
:::note Portainer
|
||||||
If you use portainer, bring down the stack in portainer. Go into the volumes section
|
If you use portainer, bring down the stack in portainer. Go into the volumes section
|
||||||
and remove all the volumes related to immcih then restart the stack.
|
and remove all the volumes related to immich then restart the stack.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
After removing the containers and volumes, the **Files** should be removed from the `UPLOAD_LOCATION` to provide a clean start.
|
After removing the containers and volumes, the **Files** should be removed from the `UPLOAD_LOCATION` to provide a clean start.
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ The recommended way to backup and restore the Immich database is to use the `pg_
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="Linux system based Backup" label="Linux system based Backup" default>
|
<TabItem value="Linux system based Backup" label="Linux system based Backup" default>
|
||||||
|
|
||||||
```bash title='Bash'
|
```bash title='Backup'
|
||||||
docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz"
|
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | gzip > "/path/to/backup/dump.sql.gz"
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash title='Restore'
|
```bash title='Restore'
|
||||||
@@ -30,7 +30,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 compose create # Create Docker containers for Immich apps without running them.
|
||||||
docker start immich_postgres # Start Postgres server
|
docker start immich_postgres # Start Postgres server
|
||||||
sleep 10 # Wait for Postgres server to start up
|
sleep 10 # Wait for Postgres server to start up
|
||||||
gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql -U postgres -d immich # Restore Backup
|
gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
|
||||||
docker compose up -d # Start remainder of Immich apps
|
docker compose up -d # Start remainder of Immich apps
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ docker compose up -d # Start remainder of Immich apps
|
|||||||
<TabItem value="Windows system based Backup" label="Windows system based Backup">
|
<TabItem value="Windows system based Backup" label="Windows system based Backup">
|
||||||
|
|
||||||
```powershell title='Backup'
|
```powershell title='Backup'
|
||||||
docker exec -t immich_postgres pg_dumpall -c -U postgres > "\path\to\backup\dump.sql"
|
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "\path\to\backup\dump.sql"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell title='Restore'
|
```powershell title='Restore'
|
||||||
@@ -47,7 +47,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 compose create # Create Docker containers for Immich apps without running them.
|
||||||
docker start immich_postgres # Start Postgres server
|
docker start immich_postgres # Start Postgres server
|
||||||
sleep 10 # Wait for Postgres server to start up
|
sleep 10 # Wait for Postgres server to start up
|
||||||
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql -U postgres -d immich # 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
|
docker compose up -d # Start remainder of Immich apps
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -63,15 +63,16 @@ services:
|
|||||||
...
|
...
|
||||||
backup:
|
backup:
|
||||||
container_name: immich_db_dumper
|
container_name: immich_db_dumper
|
||||||
image: prodrigestivill/postgres-backup-local
|
image: prodrigestivill/postgres-backup-local:14
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_HOST: database
|
POSTGRES_HOST: database
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
POSTGRES_CLUSTER: 'TRUE'
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
SCHEDULE: "@daily"
|
SCHEDULE: "@daily"
|
||||||
|
POSTGRES_EXTRA_OPTS: '--clean --if-exists'
|
||||||
BACKUP_DIR: /db_dumps
|
BACKUP_DIR: /db_dumps
|
||||||
volumes:
|
volumes:
|
||||||
- ./db_dumps:/db_dumps
|
- ./db_dumps:/db_dumps
|
||||||
@@ -82,9 +83,15 @@ services:
|
|||||||
Then you can restore with the same command but pointed at the latest dump.
|
Then you can restore with the same command but pointed at the latest dump.
|
||||||
|
|
||||||
```bash title='Automated Restore'
|
```bash title='Automated Restore'
|
||||||
gunzip < db_dumps/last/immich-latest.sql.gz | docker exec -i immich_postgres psql -U postgres -d immich
|
gunzip < db_dumps/last/immich-latest.sql.gz | docker exec -i immich_postgres psql --username=postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
If you see the error `ERROR: type "earth" does not exist`, or you have problems with Reverse Geocoding after a restore, add the following `sed` fragment to your restore command.
|
||||||
|
|
||||||
|
Example: `gunzip < "/path/to/backup/dump.sql.gz" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | docker exec -i immich_postgres psql --username=postgres`
|
||||||
|
:::
|
||||||
|
|
||||||
## Filesystem
|
## Filesystem
|
||||||
|
|
||||||
Immich stores two types of content in the filesystem: (1) original, unmodified content, and (2) generated content. Only the original content needs to be backed-up, which includes the following folders:
|
Immich stores two types of content in the filesystem: (1) original, unmodified content, and (2) generated content. Only the original content needs to be backed-up, which includes the following folders:
|
||||||
@@ -101,7 +108,7 @@ Some storage locations are impacted by the Storage Template. See below for more
|
|||||||
<TabItem value="Storage Template Off (Default)." label="Storage Template Off (Default)." default>
|
<TabItem value="Storage Template Off (Default)." label="Storage Template Off (Default)." default>
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
`UPLOAD_LOCATION/library` folder is not used by default on new machines running version 1.92.0. These are if the system administrator activated the storage template engine, for [more info](https://github.com/immich-app/immich/releases#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
|
`UPLOAD_LOCATION/library` folder is not used by default on new machines running version 1.92.0. These are if the system administrator activated the storage template engine, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
**1. User-Specific Folders:**
|
**1. User-Specific Folders:**
|
||||||
|
|||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -1,9 +1,13 @@
|
|||||||
# Jobs
|
# Jobs
|
||||||
|
|
||||||
Several Immich functionalities are implemented as jobs, which run in the background. To view the status of a job navigate to the Administration Screen, and then the `Jobs` page.
|
The `immich-server` responds to API requests for data and files for the web and mobile app. To do this quickly and reliably, it offloads most other work to `immich-microservices` in the form of _jobs_. Simply put, a job is a request to process data in the background. Jobs are picked up automatically by microservices containers.
|
||||||
|
|
||||||

|
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
|
||||||
|
|
||||||
|
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
|
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Unable to set `app.immich:/` as a valid redirect URI? See [Mobile Redirect URI](
|
|||||||
Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including:
|
Immich supports 3rd party authentication via [OpenID Connect][oidc] (OIDC), an identity layer built on top of OAuth2. OIDC is supported by most identity providers, including:
|
||||||
|
|
||||||
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
|
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
|
||||||
- [Authelia](https://www.authelia.com/configuration/identity-providers/open-id-connect/)
|
- [Authelia](https://www.authelia.com/configuration/identity-providers/openid-connect/clients/)
|
||||||
- [Okta](https://www.okta.com/openid-connect/)
|
- [Okta](https://www.okta.com/openid-connect/)
|
||||||
- [Google](https://developers.google.com/identity/openid-connect/openid-connect)
|
- [Google](https://developers.google.com/identity/openid-connect/openid-connect)
|
||||||
|
|
||||||
@@ -67,12 +67,20 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||||
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||||
| Storage Claim | string | preferred_username | Claim mapping for the user's storage label |
|
|
||||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
||||||
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
||||||
|
|
||||||
|
:::note Claim Options [1]
|
||||||
|
|
||||||
|
Claim is only used on user creation and not synchronized after that.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
The Issuer URL should look something like the following, and return a valid json document.
|
The Issuer URL should look something like the following, and return a valid json document.
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# Password Login
|
|
||||||
|
|
||||||
An overview of password login and related settings for Immich.
|
|
||||||
|
|
||||||
## Enable/Disable
|
|
||||||
|
|
||||||
Immich supports password login, which is enabled by default. The preferred way to disable it is via the [Administration Page](#administration-page), although it can also be changed via a [Server Command](#server-command) as well.
|
|
||||||
|
|
||||||
### Administration Page
|
|
||||||
|
|
||||||
To toggle the password login setting via the web, navigate to the "Administration", expand "Password Authentication", toggle the "Enabled" switch, and press "Save".
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Server Command
|
|
||||||
|
|
||||||
There are two [Server Commands](/docs/administration/server-commands.md) for password login:
|
|
||||||
|
|
||||||
1. `enable-password-login`
|
|
||||||
2. `disable-password-login`
|
|
||||||
|
|
||||||
See [Server Commands](/docs/administration/server-commands.md) for more details about how to run them.
|
|
||||||
|
|
||||||
## Password Reset
|
|
||||||
|
|
||||||
### Admin
|
|
||||||
|
|
||||||
To reset the administrator password, use the `reset-admin-password` [Server Command](/docs/administration/server-commands.md).
|
|
||||||
|
|
||||||
### User
|
|
||||||
|
|
||||||
Immich does not currently support self-service password reset. However, the administration can reset passwords for other users. See [User Management: Password Reset](/docs/administration/user-management.mdx#password-reset) for more information about how to do this.
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Pre-existing Postgres
|
||||||
|
|
||||||
|
While not officially recommended, it is possible to run Immich using a pre-existing Postgres server. To use this setup, you should have a baseline level of familiarity with Postgres and the Linux command line. If you do not have these, we recommend using the default setup with a dedicated Postgres container.
|
||||||
|
|
||||||
|
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.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Specifying the connection URL
|
||||||
|
|
||||||
|
You can connect to your pre-existing Postgres server by setting the `DB_URL` environment variable in the `.env` file.
|
||||||
|
|
||||||
|
```
|
||||||
|
DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename'
|
||||||
|
|
||||||
|
# require a SSL connection to Postgres
|
||||||
|
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require'
|
||||||
|
|
||||||
|
# require a SSL connection, but don't enforce checking the certificate name
|
||||||
|
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify'
|
||||||
|
```
|
||||||
|
|
||||||
|
## With superuser permission
|
||||||
|
|
||||||
|
Typically Immich expects superuser permission in the database, which you can grant by running `ALTER USER <immichdbusername> WITH SUPERUSER;` at the `psql` console. If you prefer not to grant superuser permissions, follow the instructions in the next section.
|
||||||
|
|
||||||
|
## Without superuser permission
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
This method is recommended for **advanced users only** and often requires manual intervention when updating Immich.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Immich can run without superuser permissions by following the below instructions at the `psql` prompt to prepare the database.
|
||||||
|
|
||||||
|
```sql title="Set up Postgres for Immich"
|
||||||
|
CREATE DATABASE <immichdatabasename>;
|
||||||
|
\c <immichdatabasename>
|
||||||
|
BEGIN;
|
||||||
|
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
||||||
|
CREATE EXTENSION vectors;
|
||||||
|
CREATE EXTENSION earthdistance CASCADE;
|
||||||
|
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||||
|
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
|
||||||
|
COMMIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating pgvecto.rs
|
||||||
|
|
||||||
|
When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`.
|
||||||
|
|
||||||
|
### Common errors
|
||||||
|
|
||||||
|
#### Permission denied for view
|
||||||
|
|
||||||
|
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat to <immichdbusername>;`.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Repair Page
|
||||||
|
|
||||||
|
The repair page is designed to give information to the system administrator about files that are not tracked, or offline paths.
|
||||||
|
|
||||||
|
## Natural State
|
||||||
|
|
||||||
|
In this situation, everything is in its place and there is no problem that the system administrator should be aware of.
|
||||||
|
|
||||||
|
<img src={require('./img/repair-page.png').default} title="server statistic" />
|
||||||
|
|
||||||
|
## Any Other Situation
|
||||||
|
|
||||||
|
:::note RAM Usage
|
||||||
|
Several users report a situation where the page fails to load. In order to solve this problem you should try to allocate more RAM to Immich, if the problem continues, you should stop using the reverse proxy while loading the page.
|
||||||
|
:::
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
In addition, you can download the information from a page, mark everything (in order to check hashing) and correct the problem if a match is found in the hashing.
|
||||||
|
|
||||||
|
<img src={require('./img/repair-page-1.png').default} title="server statistic" />
|
||||||
@@ -1,29 +1,41 @@
|
|||||||
# Reverse Proxy
|
# Reverse Proxy
|
||||||
|
|
||||||
Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Real-IP`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
The Repair page can take a long time to load. To avoid server timeouts or errors, we recommend specifying a timeout of at least 10 minutes on your proxy server.
|
||||||
|
:::
|
||||||
|
|
||||||
### Nginx example config
|
### Nginx example config
|
||||||
|
|
||||||
Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`.
|
Below is an example config for nginx. Make sure to set `public_url` to the front-facing URL of your instance, and `backend_url` to the path of the Immich server.
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
server_name <snip>
|
server_name <public_url>;
|
||||||
|
|
||||||
|
# allow large file uploads
|
||||||
client_max_body_size 50000M;
|
client_max_body_size 50000M;
|
||||||
|
|
||||||
location / {
|
# Set headers
|
||||||
proxy_pass http://<snip>:2283;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# http://nginx.org/en/docs/http/websocket.html
|
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
|
||||||
|
# set timeout
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
send_timeout 600s;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://<backend_url>:2283;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -42,15 +54,13 @@ immich.example.org {
|
|||||||
|
|
||||||
Below is an example config for Apache2 site configuration.
|
Below is an example config for Apache2 site configuration.
|
||||||
|
|
||||||
```
|
```ApacheConf
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerName <snip>
|
ServerName <snip>
|
||||||
ProxyRequests Off
|
ProxyRequests Off
|
||||||
|
# set timeout in seconds
|
||||||
ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket
|
ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket
|
||||||
ProxyPassReverse / http://127.0.0.1:2283/
|
ProxyPassReverse / http://127.0.0.1:2283/
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
|
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
|
|
||||||
**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error.
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
|
|||||||
| `reset-admin-password` | Reset the password for the admin user |
|
| `reset-admin-password` | Reset the password for the admin user |
|
||||||
| `disable-password-login` | Disable password login |
|
| `disable-password-login` | Disable password login |
|
||||||
| `enable-password-login` | Enable password login |
|
| `enable-password-login` | Enable password login |
|
||||||
|
| `enable-oauth-login` | Enable OAuth login |
|
||||||
|
| `disable-oauth-login` | Disable OAuth login |
|
||||||
| `list-users` | List Immich users |
|
| `list-users` | List Immich users |
|
||||||
|
|
||||||
## How to run a command
|
## How to run a command
|
||||||
@@ -36,13 +38,27 @@ immich-admin disable-password-login
|
|||||||
Password login has been disabled.
|
Password login has been disabled.
|
||||||
```
|
```
|
||||||
|
|
||||||
Enabled Password Login
|
Enable Password Login
|
||||||
|
|
||||||
```
|
```
|
||||||
immich-admin enable-password-login
|
immich-admin enable-password-login
|
||||||
Password login has been enabled.
|
Password login has been enabled.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Enable OAuth login
|
||||||
|
|
||||||
|
```
|
||||||
|
immich-admin enable-oauth-login
|
||||||
|
OAuth login has been enabled.
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable OAuth login
|
||||||
|
|
||||||
|
```
|
||||||
|
immich-admin disable-oauth-login
|
||||||
|
OAuth login has been disabled.
|
||||||
|
```
|
||||||
|
|
||||||
List Users
|
List Users
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Server Stats
|
||||||
|
|
||||||
|
Server statistics to show the total number of videos, photos, and usage per user.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If a storage quota has been defined for the user, the usage number will be displayed as a percentage of the total storage quota allocated to them.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::info External library
|
||||||
|
External library is not included in the storage quota.
|
||||||
|
:::
|
||||||
|
|
||||||
|
<img src={require('./img/server-stats.png').default} title="server statistic" />
|
||||||