Compare commits
490 Commits
object-sto
...
v1.100.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6d91c23f65 | ||
|
|
df02a9f5ed | ||
|
|
2702bcc407 | ||
|
|
807cd245f4 | ||
|
|
dc0f8756f5 | ||
|
|
df9ab8943d | ||
|
|
79409438a7 | ||
|
|
b15eec7ca7 | ||
|
|
908104299d | ||
|
|
c94874296c | ||
|
|
8361130351 | ||
|
|
907a95a746 | ||
|
|
57f25855d3 | ||
|
|
e02964ca0d | ||
|
|
fd301a3261 | ||
|
|
d76baee50d | ||
|
|
5e485e35e9 | ||
|
|
cfb49c8be0 | ||
|
|
2f121af9ec | ||
|
|
21feb69083 | ||
|
|
fb18129843 | ||
|
|
9fa2424652 | ||
|
|
7d1edddd51 | ||
|
|
3f18a936b2 | ||
|
|
e20d3048cd | ||
|
|
8965c25f54 | ||
|
|
7e18e69c1c | ||
|
|
d799bf7910 | ||
|
|
4272b496ff | ||
|
|
99a002b947 | ||
|
|
c8bdeb8fec | ||
|
|
8a05ff51e9 | ||
|
|
3e8af16270 | ||
|
|
45ecb629a1 | ||
|
|
943105ea20 | ||
|
|
2a75f884d9 | ||
|
|
912d723281 | ||
|
|
038e69fd02 | ||
|
|
9d3ed719e0 | ||
|
|
6ec4c5874b | ||
|
|
57758293e5 | ||
|
|
bc3979029d | ||
|
|
878932f87e | ||
|
|
a2934b8830 | ||
|
|
719dbcc4d0 | ||
|
|
cc6de7d1f1 | ||
|
|
78ece4ced9 | ||
|
|
ee58569b42 | ||
|
|
07d747221e | ||
|
|
3cd3411c1f | ||
|
|
b3b6426695 | ||
|
|
6bb30291de | ||
|
|
2c9dd18f1b | ||
|
|
07ef008b40 | ||
|
|
b3131dfe14 | ||
|
|
b4e924b0c0 | ||
|
|
01d6707b59 | ||
|
|
a224bb23d0 | ||
|
|
75947ab6c2 | ||
|
|
e3cccba78c | ||
|
|
ec55acc98c | ||
|
|
869e9f1399 | ||
|
|
46f85618db | ||
|
|
749b182f97 | ||
|
|
2ebb57cbd4 | ||
|
|
5c0c98473d | ||
|
|
546edc2e91 | ||
|
|
173b47033a | ||
|
|
d3e14fd662 | ||
|
|
06c134950a | ||
|
|
8f57bfb496 | ||
|
|
855aa8e30a | ||
|
|
f798e037d5 | ||
|
|
a1bc74cdd6 | ||
|
|
aeb7081af1 | ||
|
|
c5da317033 | ||
|
|
01f682134a | ||
|
|
43f887e5f2 | ||
|
|
ee3b3ca115 | ||
|
|
e7995014f9 | ||
|
|
a771f33fa3 | ||
|
|
397570ad1a | ||
|
|
7c34d0595e | ||
|
|
eb73f6605b | ||
|
|
bb5236ae65 | ||
|
|
e33fd40b4c | ||
|
|
73825918c0 | ||
|
|
a22bf99206 | ||
|
|
578b71b961 | ||
|
|
302d98ebe1 | ||
|
|
e338e4def6 | ||
|
|
b896d45ee7 | ||
|
|
b3c7bebbd4 | ||
|
|
e7a875eadd | ||
|
|
7f5459f050 | ||
|
|
7158706296 | ||
|
|
9b20604a70 | ||
|
|
02b9f3ee88 | ||
|
|
f14a2ae099 | ||
|
|
42ce8c5093 | ||
|
|
6690e8edf2 | ||
|
|
14ca471dea | ||
|
|
84b2fc80a4 | ||
|
|
d5ef91b1ae | ||
|
|
947bcf2d68 | ||
|
|
870d517ce3 | ||
|
|
529a83cc72 | ||
|
|
5e1498a279 | ||
|
|
ea4be83ee9 | ||
|
|
a03b37ca86 | ||
|
|
59f8a886e7 | ||
|
|
c50d318152 | ||
|
|
a8f177066b | ||
|
|
dd53795953 | ||
|
|
9e7cb52413 | ||
|
|
0795410a41 | ||
|
|
66d3daa074 | ||
|
|
ddae707ea9 | ||
|
|
4b46bb49d7 | ||
|
|
072f61927c | ||
|
|
36e5d298db | ||
|
|
3480fe5326 | ||
|
|
857ec0451d |
@@ -1,30 +1,31 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
.github/
|
||||||
|
.git/
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
docs/
|
docs/
|
||||||
|
e2e/
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
server/node_modules/
|
cli/coverage/
|
||||||
|
cli/dist/
|
||||||
|
cli/node_modules/
|
||||||
|
|
||||||
|
open-api/typescript-sdk/build/
|
||||||
|
open-api/typescript-sdk/node_modules/
|
||||||
|
|
||||||
server/coverage/
|
server/coverage/
|
||||||
server/.reverse-geocoding-dump/
|
server/node_modules/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/dist/
|
server/dist/
|
||||||
|
server/www/
|
||||||
|
server/test/assets/
|
||||||
|
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/coverage/
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
web/build/
|
||||||
|
|
||||||
cli/node_modules/
|
|
||||||
cli/.reverse-geocoding-dump/
|
|
||||||
cli/upload/
|
|
||||||
cli/dist/
|
|
||||||
|
|
||||||
e2e/
|
|
||||||
|
|
||||||
open-api/typescript-sdk/node_modules/
|
|
||||||
open-api/typescript-sdk/build/
|
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ max_line_length = off
|
|||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
quote_type = double
|
quote_type = single
|
||||||
|
|||||||
2
.gitattributes
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/build-mobile.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/cli.yml
vendored
@@ -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.0.0
|
uses: docker/setup-buildx-action@v3.2.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
|
||||||
|
|||||||
26
.github/workflows/dispatch_sdk_update.yml
vendored
@@ -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'
|
|
||||||
})
|
|
||||||
10
.github/workflows/docker.yml
vendored
@@ -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.0.0
|
uses: docker/setup-buildx-action@v3.2.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 }}
|
||||||
|
|||||||
13
.github/workflows/prepare-release.yml
vendored
@@ -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
|
||||||
|
|||||||
31
.github/workflows/sdk.yml
vendored
Normal file
@@ -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 }}
|
||||||
6
.github/workflows/static_analysis.yml
vendored
@@ -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
|
||||||
@@ -33,6 +33,10 @@ jobs:
|
|||||||
run: dart analyze --fatal-infos
|
run: dart analyze --fatal-infos
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
|
- name: Run dart format
|
||||||
|
run: dart format lib/ --set-exit-if-changed
|
||||||
|
working-directory: ./mobile
|
||||||
|
|
||||||
# Enable after riverpod generator migration is completed
|
# Enable after riverpod generator migration is completed
|
||||||
# - name: Run dart custom lint
|
# - name: Run dart custom lint
|
||||||
# run: dart run custom_lint
|
# run: dart run custom_lint
|
||||||
|
|||||||
136
.github/workflows/test.yml
vendored
@@ -10,32 +10,15 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
server-e2e-api:
|
|
||||||
name: Server (e2e-api)
|
|
||||||
runs-on: mich
|
|
||||||
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: mich
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- 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() }}
|
||||||
@@ -135,38 +114,6 @@ jobs:
|
|||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-e2e-tests:
|
|
||||||
name: CLI (e2e)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./cli
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: "recursive"
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
|
||||||
run: npm ci && npm run build
|
|
||||||
working-directory: ./open-api/typescript-sdk
|
|
||||||
|
|
||||||
- name: Run npm install (cli)
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run npm install (server)
|
|
||||||
run: npm ci && npm run build
|
|
||||||
working-directory: ./server
|
|
||||||
|
|
||||||
- name: Run e2e tests
|
|
||||||
run: npm run test:e2e
|
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
name: Web
|
name: Web
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -205,8 +152,8 @@ jobs:
|
|||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-e2e-tests:
|
e2e-tests:
|
||||||
name: Web (e2e)
|
name: End-to-End Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -215,23 +162,55 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- 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
|
||||||
|
run: npm ci && npm run build
|
||||||
|
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 -f docker/docker-compose.e2e.yml build
|
run: docker compose build
|
||||||
working-directory: ./
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests (api & cli)
|
||||||
|
run: npm run test
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -241,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
|
||||||
@@ -260,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
|
||||||
@@ -277,6 +256,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
poetry run pytest app --cov=app --cov-report term-missing
|
poetry run pytest app --cov=app --cov-report term-missing
|
||||||
|
|
||||||
|
shellcheck:
|
||||||
|
name: ShellCheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run ShellCheck
|
||||||
|
uses: ludeeus/action-shellcheck@master
|
||||||
|
with:
|
||||||
|
ignore_paths: >-
|
||||||
|
**/open-api/**
|
||||||
|
**/openapi/**
|
||||||
|
**/node_modules/**
|
||||||
|
|
||||||
generated-api-up-to-date:
|
generated-api-up-to-date:
|
||||||
name: OpenAPI Clients
|
name: OpenAPI Clients
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -285,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: |
|
||||||
@@ -337,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: |
|
||||||
@@ -358,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'
|
||||||
|
|||||||
34
.vscode/settings.json
vendored
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Makefile
@@ -19,12 +19,9 @@ 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 ./docker/docker-compose.e2e.yml up --build -V --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|||||||
36
README.md
@@ -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
|
||||||
@@ -128,3 +128,13 @@ If you feel like this is the right cause and the app is something you are seeing
|
|||||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
<a href="https://star-history.com/#immich-app/immich&Date">
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
|
||||||
},
|
},
|
||||||
ignorePatterns: ['.eslintrc.js'],
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
rules: {
|
rules: {
|
||||||
@@ -20,13 +19,9 @@ 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',
|
||||||
curly: 2,
|
curly: 2,
|
||||||
'prettier/prettier': 0,
|
'prettier/prettier': 0,
|
||||||
'unicorn/prevent-abbreviations': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
ignore: ['\\.e2e-spec$', /^ignore/i],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
**/*.spec.js
|
**/*.spec.js
|
||||||
test/**
|
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 as core
|
FROM node:20-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975f1a2d4def 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 ./
|
||||||
|
|||||||
6438
cli/package-lock.json
generated
@@ -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,13 +14,13 @@
|
|||||||
],
|
],
|
||||||
"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/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"@vitest/coverage-v8": "^1.2.2",
|
"@vitest/coverage-v8": "^1.2.2",
|
||||||
"byte-size": "^8.1.1",
|
"byte-size": "^8.1.1",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -30,26 +30,25 @@
|
|||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^51.0.0",
|
"eslint-plugin-unicorn": "^51.0.0",
|
||||||
"glob": "^10.3.1",
|
"glob": "^10.3.1",
|
||||||
"immich": "file:../server",
|
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"prepack": "npm run build",
|
"prepack": "npm run build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:cov": "vitest --coverage",
|
"test:cov": "vitest --coverage",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit"
|
||||||
"test:e2e": "vitest --config test/e2e/vitest.config.ts"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -58,5 +57,8 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash-es": "^4.17.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
329
cli/src/commands/asset.ts
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
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 files = await scan(paths, options);
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('No files found, exiting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newFiles, duplicates } = await checkForDuplicates(files, 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 }: UploadOptionsDto) => {
|
||||||
|
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 totalSizeUploaded = 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);
|
||||||
|
totalSizeUploaded += stats.size ?? 0;
|
||||||
|
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
||||||
|
newAssets.push({ id: response.id, filepath });
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uploadProgress.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully uploaded ${newAssets.length} asset${s(newAssets.length)} (${byteSize(totalSizeUploaded)})`);
|
||||||
|
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;
|
||||||
|
};
|
||||||
48
cli/src/commands/auth.ts
Normal file
@@ -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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
cli/src/commands/server-info.ts
Normal file
@@ -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,274 +0,0 @@
|
|||||||
import byteSize from 'byte-size';
|
|
||||||
import cliProgress from 'cli-progress';
|
|
||||||
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';
|
|
||||||
|
|
||||||
class Asset {
|
|
||||||
readonly path: string;
|
|
||||||
readonly deviceId!: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UploadCommand extends BaseCommand {
|
|
||||||
uploadLength!: number;
|
|
||||||
|
|
||||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
|
||||||
const api = await this.connect();
|
|
||||||
|
|
||||||
const formatResponse = await api.getSupportedMediaTypes();
|
|
||||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
|
||||||
|
|
||||||
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 crawlService.crawl({
|
|
||||||
pathsToCrawl: paths,
|
|
||||||
recursive: options.recursive,
|
|
||||||
exclusionPatterns: options.exclusionPatterns,
|
|
||||||
includeHidden: options.includeHidden,
|
|
||||||
});
|
|
||||||
|
|
||||||
files.push(...inputFiles);
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
console.log('No assets found, exiting');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsToUpload = files.map((path) => new Asset(path));
|
|
||||||
|
|
||||||
const uploadProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: '{bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}: {filename}',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
|
|
||||||
let totalSize = 0;
|
|
||||||
let sizeSoFar = 0;
|
|
||||||
|
|
||||||
let totalSizeUploaded = 0;
|
|
||||||
let uploadCounter = 0;
|
|
||||||
|
|
||||||
for (const asset of assetsToUpload) {
|
|
||||||
// Compute total size first
|
|
||||||
await asset.prepare();
|
|
||||||
totalSize += asset.fileSize;
|
|
||||||
|
|
||||||
if (options.albumName) {
|
|
||||||
asset.albumName = options.albumName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingAlbums = await api.getAllAlbums();
|
|
||||||
|
|
||||||
uploadProgress.start(totalSize, 0);
|
|
||||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const asset of assetsToUpload) {
|
|
||||||
uploadProgress.update({
|
|
||||||
filename: asset.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
let skipUpload = false;
|
|
||||||
|
|
||||||
let skipAsset = false;
|
|
||||||
let existingAssetId: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (!options.skipHash) {
|
|
||||||
const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
|
|
||||||
|
|
||||||
const checkResponse = await api.checkBulkUpload(assetBulkUploadCheckDto);
|
|
||||||
|
|
||||||
skipUpload = checkResponse.results[0].action === 'reject';
|
|
||||||
|
|
||||||
const isDuplicate = checkResponse.results[0].reason === 'duplicate';
|
|
||||||
if (isDuplicate) {
|
|
||||||
existingAssetId = checkResponse.results[0].assetId;
|
|
||||||
}
|
|
||||||
|
|
||||||
skipAsset = skipUpload && !isDuplicate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipAsset && !options.dryRun) {
|
|
||||||
if (!skipUpload) {
|
|
||||||
const formData = await asset.getUploadFormData();
|
|
||||||
const response = await this.uploadAsset(api, formData);
|
|
||||||
const json = await response.json();
|
|
||||||
existingAssetId = json.id;
|
|
||||||
uploadCounter++;
|
|
||||||
totalSizeUploaded += asset.fileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((options.album || options.albumName) && asset.albumName !== undefined) {
|
|
||||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
|
||||||
if (!album) {
|
|
||||||
const response = await api.createAlbum({ albumName: asset.albumName });
|
|
||||||
album = response;
|
|
||||||
existingAlbums.push(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingAssetId) {
|
|
||||||
await api.addAssetsToAlbum(album.id, {
|
|
||||||
ids: [existingAssetId],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeSoFar += asset.fileSize;
|
|
||||||
|
|
||||||
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
uploadProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
|
||||||
|
|
||||||
if (uploadCounter === 0) {
|
|
||||||
console.log('All assets were already uploaded, nothing to do.');
|
|
||||||
} else {
|
|
||||||
console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
|
|
||||||
}
|
|
||||||
if (options.delete) {
|
|
||||||
if (options.dryRun) {
|
|
||||||
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
|
||||||
} else {
|
|
||||||
console.log('Deleting assets that have been uploaded...');
|
|
||||||
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
|
|
||||||
deletionProgress.start(files.length, 0);
|
|
||||||
|
|
||||||
for (const asset of assetsToUpload) {
|
|
||||||
if (!options.dryRun) {
|
|
||||||
await asset.delete();
|
|
||||||
}
|
|
||||||
deletionProgress.increment();
|
|
||||||
}
|
|
||||||
deletionProgress.stop();
|
|
||||||
console.log('Deletion complete');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadAsset(api: ImmichApi, data: FormData): Promise<Response> {
|
|
||||||
const url = api.instanceUrl + '/asset/upload';
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'post',
|
|
||||||
redirect: 'error',
|
|
||||||
headers: {
|
|
||||||
'x-api-key': api.apiKey,
|
|
||||||
},
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
if (response.status !== 200 && response.status !== 201) {
|
|
||||||
throw new Error(await response.text());
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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/');
|
||||||
|
|
||||||
@@ -15,17 +14,37 @@ const program = new Command()
|
|||||||
.version(version)
|
.version(version)
|
||||||
.description('Command line interface for Immich')
|
.description('Command line interface for Immich')
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option('-d, --config-directory', 'Configuration directory where auth.yml will be stored')
|
new Option('-d, --config-directory <directory>', 'Configuration directory where auth.yml will be stored')
|
||||||
.env('IMMICH_CONFIG_DIR')
|
.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(
|
||||||
@@ -43,34 +62,13 @@ program
|
|||||||
.env('IMMICH_DRY_RUN')
|
.env('IMMICH_DRY_RUN')
|
||||||
.default(false),
|
.default(false),
|
||||||
)
|
)
|
||||||
|
.addOption(
|
||||||
|
new Option('-c, --concurrency <number>', 'Number of assets to upload at the same time')
|
||||||
|
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||||
|
.default(4),
|
||||||
|
)
|
||||||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
.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('[instanceUrl]')
|
|
||||||
.argument('[apiKey]')
|
|
||||||
.action(async (paths, options) => {
|
|
||||||
await new LoginCommand(program.opts()).run(paths, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
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,111 +0,0 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import yaml from 'yaml';
|
|
||||||
import {
|
|
||||||
TEST_AUTH_FILE,
|
|
||||||
TEST_CONFIG_DIR,
|
|
||||||
TEST_IMMICH_API_KEY,
|
|
||||||
TEST_IMMICH_INSTANCE_URL,
|
|
||||||
createTestAuthFile,
|
|
||||||
deleteAuthFile,
|
|
||||||
readTestAuthFile,
|
|
||||||
spyOnConsole,
|
|
||||||
} from '../../test/cli-test-utils';
|
|
||||||
import { SessionService } from './session.service';
|
|
||||||
|
|
||||||
const 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);
|
||||||
172
cli/src/utils.ts
Normal 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')));
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { ImmichApi } from 'src/services/api.service';
|
|
||||||
|
|
||||||
export const TEST_CONFIG_DIR = '/tmp/immich/';
|
|
||||||
export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
|
|
||||||
export const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
|
|
||||||
export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
|
|
||||||
|
|
||||||
export const CLI_BASE_OPTIONS = { configDirectory: TEST_CONFIG_DIR };
|
|
||||||
|
|
||||||
export const setup = async () => {
|
|
||||||
const api = new ImmichApi(process.env.IMMICH_INSTANCE_URL as string, '');
|
|
||||||
await api.signUpAdmin({ email: 'cli@immich.app', password: 'password', name: 'Administrator' });
|
|
||||||
const admin = await api.login({ email: 'cli@immich.app', password: 'password' });
|
|
||||||
const apiKey = await api.createApiKey(
|
|
||||||
{ name: 'CLI Test' },
|
|
||||||
{ headers: { Authorization: `Bearer ${admin.accessToken}` } },
|
|
||||||
);
|
|
||||||
|
|
||||||
api.setApiKey(apiKey.secret);
|
|
||||||
|
|
||||||
return api;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
||||||
|
|
||||||
export const createTestAuthFile = async (contents: string) => {
|
|
||||||
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
|
||||||
// Create config folder if it doesn't exist
|
|
||||||
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
|
|
||||||
if (!created) {
|
|
||||||
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(TEST_AUTH_FILE, contents);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readTestAuthFile = async (): Promise<string> => {
|
|
||||||
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteAuthFile = () => {
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(TEST_AUTH_FILE);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { restoreTempFolder, testApp } from '@test-utils';
|
|
||||||
import { readFile, stat } from 'node:fs/promises';
|
|
||||||
import { CLI_BASE_OPTIONS, TEST_AUTH_FILE, deleteAuthFile, setup, spyOnConsole } from 'test/cli-test-utils';
|
|
||||||
import yaml from 'yaml';
|
|
||||||
import { LoginCommand } from '../../src/commands/login.command';
|
|
||||||
|
|
||||||
describe(`login-key (e2e)`, () => {
|
|
||||||
let apiKey: string;
|
|
||||||
let instanceUrl: string;
|
|
||||||
|
|
||||||
spyOnConsole();
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await testApp.create();
|
|
||||||
if (process.env.IMMICH_INSTANCE_URL) {
|
|
||||||
instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
|
||||||
} else {
|
|
||||||
throw new Error('IMMICH_INSTANCE_URL environment variable not set');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await testApp.teardown();
|
|
||||||
await restoreTempFolder();
|
|
||||||
deleteAuthFile();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await testApp.reset();
|
|
||||||
await restoreTempFolder();
|
|
||||||
|
|
||||||
const api = await setup();
|
|
||||||
apiKey = api.apiKey;
|
|
||||||
|
|
||||||
deleteAuthFile();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error when providing an invalid API key', async () => {
|
|
||||||
await expect(new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow(
|
|
||||||
`Failed to connect to server ${instanceUrl}: Error: 401`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should log in when providing the correct API key', async () => {
|
|
||||||
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an auth file when logging in', async () => {
|
|
||||||
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey);
|
|
||||||
|
|
||||||
const data: string = await readFile(TEST_AUTH_FILE, 'utf8');
|
|
||||||
const parsedConfig = yaml.parse(data);
|
|
||||||
|
|
||||||
expect(parsedConfig).toEqual(expect.objectContaining({ instanceUrl, apiKey }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create an auth file with chmod 600', async () => {
|
|
||||||
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey);
|
|
||||||
|
|
||||||
const stats = await stat(TEST_AUTH_FILE);
|
|
||||||
const mode = (stats.mode & 0o777).toString(8);
|
|
||||||
|
|
||||||
expect(mode).toEqual('600');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { restoreTempFolder, testApp } from '@test-utils';
|
|
||||||
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils';
|
|
||||||
import { ServerInfoCommand } from '../../src/commands/server-info.command';
|
|
||||||
|
|
||||||
describe(`server-info (e2e)`, () => {
|
|
||||||
const consoleSpy = spyOnConsole();
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await testApp.create();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await testApp.teardown();
|
|
||||||
await restoreTempFolder();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await testApp.reset();
|
|
||||||
await restoreTempFolder();
|
|
||||||
const api = await setup();
|
|
||||||
process.env.IMMICH_API_KEY = api.apiKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show server version', async () => {
|
|
||||||
await new ServerInfoCommand(CLI_BASE_OPTIONS).run();
|
|
||||||
|
|
||||||
expect(consoleSpy.mock.calls).toEqual([
|
|
||||||
[expect.stringMatching(new RegExp('Server Version: \\d+.\\d+.\\d+'))],
|
|
||||||
[expect.stringMatching('Image Types: .*')],
|
|
||||||
[expect.stringMatching('Video Types: .*')],
|
|
||||||
['Statistics:\n Images: 0\n Videos: 0\n Total: 0'],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
|
||||||
import { access } from 'node:fs/promises';
|
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
export const directoryExists = (directory: string) =>
|
|
||||||
access(directory)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
let IMMICH_TEST_ASSET_PATH: string = '';
|
|
||||||
|
|
||||||
if (process.env.IMMICH_TEST_ASSET_PATH === undefined) {
|
|
||||||
IMMICH_TEST_ASSET_PATH = path.normalize(`${__dirname}/../../../server/test/assets/`);
|
|
||||||
process.env.IMMICH_TEST_ASSET_PATH = IMMICH_TEST_ASSET_PATH;
|
|
||||||
} else {
|
|
||||||
IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) {
|
|
||||||
throw new Error(
|
|
||||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.DB_HOSTNAME === undefined) {
|
|
||||||
// DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container.
|
|
||||||
const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
|
|
||||||
.withExposedPorts(5432)
|
|
||||||
.withDatabase('immich')
|
|
||||||
.withUsername('postgres')
|
|
||||||
.withPassword('postgres')
|
|
||||||
.withReuse()
|
|
||||||
.start();
|
|
||||||
|
|
||||||
process.env.DB_URL = pg.getConnectionUri();
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.NODE_ENV = 'development';
|
|
||||||
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../../../server/e2e/jobs/immich-e2e-config.json`);
|
|
||||||
process.env.TZ = 'Z';
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from '@test-utils';
|
|
||||||
import { ImmichApi } from 'src/services/api.service';
|
|
||||||
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils';
|
|
||||||
import { UploadCommand } from '../../src/commands/upload.command';
|
|
||||||
|
|
||||||
describe(`upload (e2e)`, () => {
|
|
||||||
let api: ImmichApi;
|
|
||||||
|
|
||||||
spyOnConsole();
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await testApp.create();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await testApp.teardown();
|
|
||||||
await restoreTempFolder();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await testApp.reset();
|
|
||||||
await restoreTempFolder();
|
|
||||||
api = await setup();
|
|
||||||
process.env.IMMICH_API_KEY = api.apiKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should upload a folder recursively', async () => {
|
|
||||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
|
|
||||||
const assets = await api.getAllAssets();
|
|
||||||
expect(assets.length).toBeGreaterThan(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create a new album', async () => {
|
|
||||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
|
|
||||||
const albums = await api.getAllAlbums();
|
|
||||||
expect(albums.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create album from folder name', async () => {
|
|
||||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
|
||||||
recursive: true,
|
|
||||||
album: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const albums = await api.getAllAlbums();
|
|
||||||
expect(albums.length).toEqual(1);
|
|
||||||
const natureAlbum = albums[0];
|
|
||||||
expect(natureAlbum.albumName).toEqual('nature');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add existing assets to album', async () => {
|
|
||||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// upload again, but this time add to album
|
|
||||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
|
||||||
recursive: true,
|
|
||||||
album: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const albums = await api.getAllAlbums();
|
|
||||||
expect(albums.length).toEqual(1);
|
|
||||||
const natureAlbum = albums[0];
|
|
||||||
expect(natureAlbum.albumName).toEqual('nature');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should upload to the specified album name', async () => {
|
|
||||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
|
||||||
recursive: true,
|
|
||||||
albumName: 'testAlbum',
|
|
||||||
});
|
|
||||||
|
|
||||||
const albums = await api.getAllAlbums();
|
|
||||||
expect(albums.length).toEqual(1);
|
|
||||||
const testAlbum = albums[0];
|
|
||||||
expect(testAlbum.albumName).toEqual('testAlbum');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@test-utils': new URL('../../../server/dist/test-utils/utils.js', import.meta.url).pathname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
include: ['**/*.e2e-spec.ts'],
|
|
||||||
globals: true,
|
|
||||||
globalSetup: 'test/e2e/setup.ts',
|
|
||||||
pool: 'forks',
|
|
||||||
poolOptions: {
|
|
||||||
forks: {
|
|
||||||
maxForks: 1,
|
|
||||||
minForks: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
testTimeout: 10_000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module.exports = async () => {
|
|
||||||
process.env.TZ = 'UTC';
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// add all jest-extended matchers
|
|
||||||
import * as matchers from 'jest-extended';
|
|
||||||
expect.extend(matchers);
|
|
||||||
@@ -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 |
BIN
design/immich-logo-inline-dark.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
63
design/immich-logo-inline-dark.svg
Normal file
@@ -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 |
BIN
design/immich-logo-inline-light.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
62
design/immich-logo-inline-light.svg
Normal file
@@ -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 |
BIN
design/immich-logo-stacked-dark.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
66
design/immich-logo-stacked-dark.svg
Normal file
@@ -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 |
BIN
design/immich-logo-stacked-light.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
66
design/immich-logo-stacked-light.svg
Normal file
@@ -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 |
BIN
design/immich-logo.png
Normal file
|
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 |
BIN
design/immich-text-dark.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
design/immich-text-light.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 278 KiB |
BIN
design/nsc3.png
|
Before Width: | Height: | Size: 2.7 MiB |
BIN
design/nsc4.jpeg
|
Before Width: | Height: | Size: 406 KiB |
BIN
design/nsc6.png
|
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-server.sh" ]
|
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-microservices.sh" ]
|
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:5ccad477d0057e62a7cd1981ffcc43785ac10c5a35522dc207466ff7e7ec845f
|
||||||
|
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,12 +58,12 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
|||||||
@@ -38,12 +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:
|
volumes:
|
||||||
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
#- /etc/OpenCL:/etc/OpenCL:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||||
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
#- /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
|
||||||
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
|
||||||
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
|
||||||
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
|
||||||
|
|
||||||
vaapi:
|
vaapi:
|
||||||
devices:
|
devices:
|
||||||
|
|||||||
12
docker/prometheus.yml
Normal file
@@ -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-->
|
||||||
|
|||||||
@@ -288,7 +288,11 @@ 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`.
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,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:**
|
||||||
|
|||||||
BIN
docs/docs/administration/img/customize-delete-user.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/docs/administration/img/delete-user.webp
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
docs/docs/administration/img/immediately-remove-user.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/docs/administration/img/repair-page-1.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/docs/administration/img/repair-page.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/docs/administration/img/server-stats.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 47 KiB |
BIN
docs/docs/administration/img/user-quota-size.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/docs/administration/img/user-storage-label.png
Normal file
|
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.
|
|
||||||
31
docs/docs/administration/repair-page.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 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).
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
To get rid of Offline paths you can follow this [guide](/docs/guides/remove-offline-files.md)
|
||||||
|
:::
|
||||||
|
|
||||||
|
- 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" />
|
||||||