Compare commits
352 Commits
| 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 |
@@ -1,30 +1,31 @@
|
||||
.vscode/
|
||||
.github/
|
||||
.git/
|
||||
|
||||
design/
|
||||
docker/
|
||||
docs/
|
||||
e2e/
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
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/.reverse-geocoding-dump/
|
||||
server/node_modules/
|
||||
server/upload/
|
||||
server/dist/
|
||||
server/www/
|
||||
server/test/assets/
|
||||
|
||||
web/node_modules/
|
||||
web/coverage/
|
||||
web/.svelte-kit
|
||||
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
|
||||
|
||||
[*.{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 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 linguist-generated=true
|
||||
|
||||
|
||||
2
.github/workflows/build-mobile.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
flutter-version: "3.19.3"
|
||||
cache: true
|
||||
|
||||
- name: Create the Keystore
|
||||
|
||||
4
.github/workflows/cli.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
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
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.1.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
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
@@ -121,7 +115,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
15
.github/workflows/prepare-release.yml
vendored
@@ -4,16 +4,16 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
serverBump:
|
||||
description: "Bump server version"
|
||||
description: 'Bump server version'
|
||||
required: true
|
||||
default: "false"
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- "false"
|
||||
- 'false'
|
||||
- minor
|
||||
- patch
|
||||
mobileBump:
|
||||
description: "Bump mobile build number"
|
||||
description: 'Bump mobile build number'
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
@@ -46,8 +46,8 @@ jobs:
|
||||
with:
|
||||
author_name: Alex The Bot
|
||||
author_email: alex.tran1502@gmail.com
|
||||
default_author: user_info
|
||||
message: "Version ${{ env.IMMICH_VERSION }}"
|
||||
default_author: user_info
|
||||
message: 'Version ${{ env.IMMICH_VERSION }}'
|
||||
tag: ${{ env.IMMICH_VERSION }}
|
||||
push: true
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
name: release-apk-signed
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.IMMICH_VERSION }}
|
||||
@@ -85,4 +85,5 @@ jobs:
|
||||
docker/example.env
|
||||
docker/hwaccel.ml.yml
|
||||
docker/hwaccel.transcoding.yml
|
||||
docker/prometheus.yml
|
||||
*.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 }}
|
||||
2
.github/workflows/static_analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
flutter-version: "3.19.3"
|
||||
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
|
||||
68
.github/workflows/test.yml
vendored
@@ -10,23 +10,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
server-e2e-api:
|
||||
name: Server (e2e-api)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./server
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Run e2e tests
|
||||
run: npm run e2e:api
|
||||
|
||||
server-e2e-jobs:
|
||||
name: Server (e2e-jobs)
|
||||
runs-on: ubuntu-latest
|
||||
@@ -35,7 +18,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: make server-e2e-jobs
|
||||
@@ -108,17 +91,13 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
- name: Setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Run npm install (cli)
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Run npm install (server)
|
||||
run: npm ci
|
||||
working-directory: ./server
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -184,7 +163,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -194,25 +173,44 @@ jobs:
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
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
|
||||
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
|
||||
run: npx playwright install --with-deps
|
||||
run: npx playwright install --with-deps chromium
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Docker build
|
||||
run: docker compose build
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run e2e tests (api & cli)
|
||||
run: npm run test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run e2e tests (web)
|
||||
run: npx playwright test
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
mobile-unit-tests:
|
||||
name: Mobile
|
||||
@@ -222,8 +220,8 @@ jobs:
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
channel: 'stable'
|
||||
flutter-version: '3.16.9'
|
||||
- name: Run tests
|
||||
working-directory: ./mobile
|
||||
run: flutter test -j 1
|
||||
@@ -241,7 +239,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: "poetry"
|
||||
cache: 'poetry'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev --with cpu
|
||||
@@ -279,7 +277,7 @@ jobs:
|
||||
- name: Run API generation
|
||||
run: make open-api
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@v18
|
||||
uses: tj-actions/verify-changed-files@v19
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
@@ -331,14 +329,14 @@ jobs:
|
||||
|
||||
- name: Generate new migrations
|
||||
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
|
||||
uses: tj-actions/verify-changed-files@v18
|
||||
uses: tj-actions/verify-changed-files@v19
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
server/src/infra/migrations/
|
||||
server/src/migrations/
|
||||
- name: Verify migration files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
@@ -352,11 +350,11 @@ jobs:
|
||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@v18
|
||||
uses: tj-actions/verify-changed-files@v19
|
||||
id: verify-changed-sql-files
|
||||
with:
|
||||
files: |
|
||||
server/src/infra/sql
|
||||
server/src/queries
|
||||
|
||||
- name: Verify SQL files have not changed
|
||||
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"
|
||||
}
|
||||
}
|
||||
3
Makefile
@@ -19,9 +19,6 @@ pull-stage:
|
||||
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
|
||||
|
||||
server-e2e-api:
|
||||
npm run e2e:api --prefix server
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
34
README.md
@@ -9,26 +9,26 @@
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<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/>
|
||||
<a href="https://immich.app">
|
||||
<img src="design/immich-screenshots.png" title="Main Screenshot">
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="readme_i18n/README_ca_ES.md">Català</a>
|
||||
<a href="readme_i18n/README_es_ES.md">Español</a>
|
||||
<a href="readme_i18n/README_fr_FR.md">Français</a>
|
||||
<a href="readme_i18n/README_it_IT.md">Italiano</a>
|
||||
<a href="readme_i18n/README_ja_JP.md">日本語</a>
|
||||
<a href="readme_i18n/README_ko_KR.md">한국어</a>
|
||||
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
|
||||
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
|
||||
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
|
||||
<a href="readme_i18n/README_zh_CN.md">中文</a>
|
||||
<a href="readme_i18n/README_ru_RU.md">Русский</a>
|
||||
</p>
|
||||
|
||||
## Disclaimer
|
||||
@@ -131,6 +131,10 @@ If you feel like this is the right cause and the app is something you are seeing
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#immich-app/immich">
|
||||
<img src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" alt="Star History Chart" width="100%" />
|
||||
<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>
|
||||
|
||||
@@ -19,8 +19,9 @@ module.exports = {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/no-process-exit': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'error',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
**/*.spec.js
|
||||
coverage/**
|
||||
src/**
|
||||
upload/**
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
.eslintrc.cjs
|
||||
.gitignore
|
||||
.prettierignore
|
||||
.prettierrc
|
||||
Dockerfile
|
||||
package-lock.json
|
||||
testSetup.js
|
||||
tsconfig.json
|
||||
tsconfig.build.json
|
||||
vite.config.ts
|
||||
vitest.config.ts
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c as core
|
||||
FROM node:20-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975f1a2d4def as core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
6072
cli/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.0.8",
|
||||
"version": "2.2.0",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -14,7 +14,6 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@testcontainers/postgresql": "^10.7.1",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
@@ -36,6 +35,7 @@
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.2.2",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
|
||||
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,444 +0,0 @@
|
||||
import { AssetBulkUploadCheckResult } from '@immich/sdk';
|
||||
import byteSize from 'byte-size';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { chunk, zip } from 'lodash-es';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs, { createReadStream } from 'node:fs';
|
||||
import { access, constants, stat, unlink } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import { basename } from 'node:path';
|
||||
import { ImmichApi } from 'src/services/api.service';
|
||||
import { CrawlService } from '../services/crawl.service';
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
const zipDefined = zip as <T, U>(a: T[], b: U[]) => [T, U][];
|
||||
|
||||
enum CheckResponseStatus {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
DUPLICATE = 'duplicate',
|
||||
}
|
||||
|
||||
class Asset {
|
||||
readonly path: string;
|
||||
|
||||
id?: string;
|
||||
deviceAssetId?: string;
|
||||
fileCreatedAt?: Date;
|
||||
fileModifiedAt?: Date;
|
||||
sidecarPath?: string;
|
||||
fileSize?: number;
|
||||
albumName?: string;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
const stats = await stat(this.path);
|
||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, '');
|
||||
this.fileCreatedAt = stats.mtime;
|
||||
this.fileModifiedAt = stats.mtime;
|
||||
this.fileSize = stats.size;
|
||||
this.albumName = this.extractAlbumName();
|
||||
}
|
||||
|
||||
async getUploadFormData(): Promise<FormData> {
|
||||
if (!this.deviceAssetId) {
|
||||
throw new Error('Device asset id not set');
|
||||
}
|
||||
if (!this.fileCreatedAt) {
|
||||
throw new Error('File created at not set');
|
||||
}
|
||||
if (!this.fileModifiedAt) {
|
||||
throw new Error('File modified at not set');
|
||||
}
|
||||
|
||||
// TODO: doesn't xmp replace the file extension? Will need investigation
|
||||
const sideCarPath = `${this.path}.xmp`;
|
||||
let sidecarData: Blob | undefined = undefined;
|
||||
try {
|
||||
await access(sideCarPath, constants.R_OK);
|
||||
sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath));
|
||||
} catch {}
|
||||
|
||||
const data: any = {
|
||||
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
||||
deviceAssetId: this.deviceAssetId,
|
||||
deviceId: 'CLI',
|
||||
fileCreatedAt: this.fileCreatedAt,
|
||||
fileModifiedAt: this.fileModifiedAt,
|
||||
isFavorite: String(false),
|
||||
};
|
||||
const formData = new FormData();
|
||||
|
||||
for (const property in data) {
|
||||
formData.append(property, data[property]);
|
||||
}
|
||||
|
||||
if (sidecarData) {
|
||||
formData.append('sidecarData', sidecarData);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
return unlink(this.path);
|
||||
}
|
||||
|
||||
public async hash(): Promise<string> {
|
||||
const sha1 = (filePath: string) => {
|
||||
const hash = createHash('sha1');
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const rs = createReadStream(filePath);
|
||||
rs.on('error', reject);
|
||||
rs.on('data', (chunk) => hash.update(chunk));
|
||||
rs.on('end', () => resolve(hash.digest('hex')));
|
||||
});
|
||||
};
|
||||
|
||||
return await sha1(this.path);
|
||||
}
|
||||
|
||||
private extractAlbumName(): string | undefined {
|
||||
return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2);
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadOptionsDto {
|
||||
recursive? = false;
|
||||
exclusionPatterns?: string[] = [];
|
||||
dryRun? = false;
|
||||
skipHash? = false;
|
||||
delete? = false;
|
||||
album? = false;
|
||||
albumName? = '';
|
||||
includeHidden? = false;
|
||||
concurrency? = 4;
|
||||
}
|
||||
|
||||
export class UploadCommand extends BaseCommand {
|
||||
api!: ImmichApi;
|
||||
|
||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||
this.api = await this.connect();
|
||||
|
||||
console.log('Crawling for assets...');
|
||||
const files = await this.getFiles(paths, options);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No assets found, exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const assetsToCheck = files.map((path) => new Asset(path));
|
||||
|
||||
const { newAssets, duplicateAssets } = await this.checkAssets(assetsToCheck, options.concurrency ?? 4);
|
||||
|
||||
const totalSizeUploaded = await this.upload(newAssets, options);
|
||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
||||
if (newAssets.length === 0) {
|
||||
console.log('All assets were already uploaded, nothing to do.');
|
||||
} else {
|
||||
console.log(
|
||||
`${messageStart} uploaded ${newAssets.length} asset${newAssets.length === 1 ? '' : 's'} (${byteSize(totalSizeUploaded)})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.album || options.albumName) {
|
||||
const { createdAlbumCount, updatedAssetCount } = await this.updateAlbums(
|
||||
[...newAssets, ...duplicateAssets],
|
||||
options,
|
||||
);
|
||||
console.log(`${messageStart} created ${createdAlbumCount} new album${createdAlbumCount === 1 ? '' : 's'}`);
|
||||
console.log(`${messageStart} updated ${updatedAssetCount} asset${updatedAssetCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
|
||||
if (!options.delete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Deleting assets that have been uploaded...');
|
||||
|
||||
await this.deleteAssets(newAssets, options);
|
||||
}
|
||||
|
||||
public async checkAssets(
|
||||
assetsToCheck: Asset[],
|
||||
concurrency: number,
|
||||
): Promise<{ newAssets: Asset[]; duplicateAssets: Asset[]; rejectedAssets: Asset[] }> {
|
||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
||||
await Promise.all(assets.map((asset: Asset) => asset.prepare()));
|
||||
}
|
||||
|
||||
const checkProgress = new cliProgress.SingleBar(
|
||||
{ format: 'Checking assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
checkProgress.start(assetsToCheck.length, 0);
|
||||
|
||||
const newAssets = [];
|
||||
const duplicateAssets = [];
|
||||
const rejectedAssets = [];
|
||||
try {
|
||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
||||
const checkedAssets = await this.getStatus(assets);
|
||||
for (const checked of checkedAssets) {
|
||||
if (checked.status === CheckResponseStatus.ACCEPT) {
|
||||
newAssets.push(checked.asset);
|
||||
} else if (checked.status === CheckResponseStatus.DUPLICATE) {
|
||||
duplicateAssets.push(checked.asset);
|
||||
} else {
|
||||
rejectedAssets.push(checked.asset);
|
||||
}
|
||||
checkProgress.increment();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
checkProgress.stop();
|
||||
}
|
||||
|
||||
return { newAssets, duplicateAssets, rejectedAssets };
|
||||
}
|
||||
|
||||
public async upload(assetsToUpload: Asset[], options: UploadOptionsDto): Promise<number> {
|
||||
let totalSize = 0;
|
||||
|
||||
// Compute total size first
|
||||
for (const asset of assetsToUpload) {
|
||||
totalSize += asset.fileSize ?? 0;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
const uploadProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
||||
let totalSizeUploaded = 0;
|
||||
try {
|
||||
for (const assets of chunk(assetsToUpload, options.concurrency)) {
|
||||
const ids = await this.uploadAssets(assets);
|
||||
for (const [asset, id] of zipDefined(assets, ids)) {
|
||||
asset.id = id;
|
||||
if (asset.fileSize) {
|
||||
totalSizeUploaded += asset.fileSize ?? 0;
|
||||
} else {
|
||||
console.log(`Could not determine file size for ${asset.path}`);
|
||||
}
|
||||
}
|
||||
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
||||
}
|
||||
} finally {
|
||||
uploadProgress.stop();
|
||||
}
|
||||
|
||||
return totalSizeUploaded;
|
||||
}
|
||||
|
||||
public async getFiles(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
||||
const inputFiles: string[] = [];
|
||||
for (const pathArgument of paths) {
|
||||
const fileStat = await fs.promises.lstat(pathArgument);
|
||||
if (fileStat.isFile()) {
|
||||
inputFiles.push(pathArgument);
|
||||
}
|
||||
}
|
||||
|
||||
const files: string[] = await this.crawl(paths, options);
|
||||
files.push(...inputFiles);
|
||||
return files;
|
||||
}
|
||||
|
||||
public async getAlbums(): Promise<Map<string, string>> {
|
||||
const existingAlbums = await this.api.getAllAlbums();
|
||||
|
||||
const albumMapping = new Map<string, string>();
|
||||
for (const album of existingAlbums) {
|
||||
albumMapping.set(album.albumName, album.id);
|
||||
}
|
||||
|
||||
return albumMapping;
|
||||
}
|
||||
|
||||
public async updateAlbums(
|
||||
assets: Asset[],
|
||||
options: UploadOptionsDto,
|
||||
): Promise<{ createdAlbumCount: number; updatedAssetCount: number }> {
|
||||
if (options.albumName) {
|
||||
for (const asset of assets) {
|
||||
asset.albumName = options.albumName;
|
||||
}
|
||||
}
|
||||
|
||||
const existingAlbums = await this.getAlbums();
|
||||
const assetsToUpdate = assets.filter(
|
||||
(asset): asset is Asset & { albumName: string; id: string } => !!(asset.albumName && asset.id),
|
||||
);
|
||||
|
||||
const newAlbumsSet: Set<string> = new Set();
|
||||
for (const asset of assetsToUpdate) {
|
||||
if (!existingAlbums.has(asset.albumName)) {
|
||||
newAlbumsSet.add(asset.albumName);
|
||||
}
|
||||
}
|
||||
|
||||
const newAlbums = [...newAlbumsSet];
|
||||
|
||||
if (options.dryRun) {
|
||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
||||
}
|
||||
|
||||
const albumCreationProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
albumCreationProgress.start(newAlbums.length, 0);
|
||||
|
||||
try {
|
||||
for (const albumNames of chunk(newAlbums, options.concurrency)) {
|
||||
const newAlbumIds = await Promise.all(
|
||||
albumNames.map((albumName: string) => this.api.createAlbum({ albumName }).then((r) => r.id)),
|
||||
);
|
||||
|
||||
for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) {
|
||||
existingAlbums.set(albumName, albumId);
|
||||
}
|
||||
|
||||
albumCreationProgress.increment(albumNames.length);
|
||||
}
|
||||
} finally {
|
||||
albumCreationProgress.stop();
|
||||
}
|
||||
|
||||
const albumToAssets = new Map<string, string[]>();
|
||||
for (const asset of assetsToUpdate) {
|
||||
const albumId = existingAlbums.get(asset.albumName);
|
||||
if (albumId) {
|
||||
if (!albumToAssets.has(albumId)) {
|
||||
albumToAssets.set(albumId, []);
|
||||
}
|
||||
albumToAssets.get(albumId)?.push(asset.id);
|
||||
}
|
||||
}
|
||||
|
||||
const albumUpdateProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
albumUpdateProgress.start(assetsToUpdate.length, 0);
|
||||
|
||||
try {
|
||||
for (const [albumId, assets] of albumToAssets.entries()) {
|
||||
for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) {
|
||||
await this.api.addAssetsToAlbum(albumId, { ids: assetBatch });
|
||||
albumUpdateProgress.increment(assetBatch.length);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
albumUpdateProgress.stop();
|
||||
}
|
||||
|
||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
||||
}
|
||||
|
||||
public async deleteAssets(assets: Asset[], options: UploadOptionsDto): Promise<void> {
|
||||
const deletionProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
deletionProgress.start(assets.length, 0);
|
||||
|
||||
try {
|
||||
for (const assetBatch of chunk(assets, options.concurrency)) {
|
||||
await Promise.all(assetBatch.map((asset: Asset) => asset.delete()));
|
||||
deletionProgress.update(assetBatch.length);
|
||||
}
|
||||
} finally {
|
||||
deletionProgress.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async getStatus(assets: Asset[]): Promise<{ asset: Asset; status: CheckResponseStatus }[]> {
|
||||
const checkResponse = await this.checkHashes(assets);
|
||||
|
||||
const responses = [];
|
||||
for (const [check, asset] of zipDefined(checkResponse, assets)) {
|
||||
if (check.assetId) {
|
||||
asset.id = check.assetId;
|
||||
}
|
||||
|
||||
if (check.action === 'accept') {
|
||||
responses.push({ asset, status: CheckResponseStatus.ACCEPT });
|
||||
} else if (check.reason === 'duplicate') {
|
||||
responses.push({ asset, status: CheckResponseStatus.DUPLICATE });
|
||||
} else {
|
||||
responses.push({ asset, status: CheckResponseStatus.REJECT });
|
||||
}
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
private async checkHashes(assetsToCheck: Asset[]): Promise<AssetBulkUploadCheckResult[]> {
|
||||
const checksums = await Promise.all(assetsToCheck.map((asset) => asset.hash()));
|
||||
const assetBulkUploadCheckDto = {
|
||||
assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })),
|
||||
};
|
||||
const checkResponse = await this.api.checkBulkUpload(assetBulkUploadCheckDto);
|
||||
return checkResponse.results;
|
||||
}
|
||||
|
||||
private async uploadAssets(assets: Asset[]): Promise<string[]> {
|
||||
const fileRequests = await Promise.all(assets.map((asset) => asset.getUploadFormData()));
|
||||
return Promise.all(fileRequests.map((request) => this.uploadAsset(request).then((response) => response.id)));
|
||||
}
|
||||
|
||||
private async crawl(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
||||
const formatResponse = await this.api.getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
||||
|
||||
return crawlService.crawl({
|
||||
pathsToCrawl: paths,
|
||||
recursive: options.recursive,
|
||||
exclusionPatterns: options.exclusionPatterns,
|
||||
includeHidden: options.includeHidden,
|
||||
});
|
||||
}
|
||||
|
||||
private async uploadAsset(data: FormData): Promise<{ id: string }> {
|
||||
const url = this.api.instanceUrl + '/asset/upload';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'post',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'x-api-key': this.api.apiKey,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
import { Command, Option } from 'commander';
|
||||
import os from 'node:os';
|
||||
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 { 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/');
|
||||
|
||||
@@ -18,14 +17,34 @@ const program = new Command()
|
||||
new Option('-d, --config-directory <directory>', 'Configuration directory where auth.yml will be stored')
|
||||
.env('IMMICH_CONFIG_DIR')
|
||||
.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
|
||||
.command('upload')
|
||||
.description('Upload assets')
|
||||
.usage('[options] [paths...]')
|
||||
.usage('[paths...] [options]')
|
||||
.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, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false))
|
||||
.addOption(
|
||||
@@ -44,38 +63,12 @@ program
|
||||
.default(false),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-c, --concurrency', 'Number of assets to upload at the same time')
|
||||
new Option('-c, --concurrency <number>', 'Number of assets to upload at the same time')
|
||||
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||
.default(4),
|
||||
)
|
||||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||
.action(async (paths, options) => {
|
||||
options.exclusionPatterns = options.ignore;
|
||||
await new UploadCommand(program.opts()).run(paths, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('server-info')
|
||||
.description('Display server information')
|
||||
.action(async () => {
|
||||
await new ServerInfoCommand(program.opts()).run();
|
||||
});
|
||||
|
||||
program
|
||||
.command('login-key')
|
||||
.description('Login using an API key')
|
||||
.argument('url')
|
||||
.argument('key')
|
||||
.action(async (url, key) => {
|
||||
await new LoginCommand(program.opts()).run(url, key);
|
||||
});
|
||||
|
||||
program
|
||||
.command('logout')
|
||||
.description('Remove stored credentials')
|
||||
.action(async () => {
|
||||
await new LogoutCommand(program.opts()).run();
|
||||
});
|
||||
.action((paths, options) => upload(paths, program.opts(), options));
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import {
|
||||
ApiKeyCreateDto,
|
||||
AssetBulkUploadCheckDto,
|
||||
BulkIdsDto,
|
||||
CreateAlbumDto,
|
||||
CreateAssetDto,
|
||||
LoginCredentialDto,
|
||||
SignUpDto,
|
||||
addAssetsToAlbum,
|
||||
checkBulkUpload,
|
||||
createAlbum,
|
||||
createApiKey,
|
||||
getAllAlbums,
|
||||
getAllAssets,
|
||||
getAssetStatistics,
|
||||
getMyUserInfo,
|
||||
getServerVersion,
|
||||
getSupportedMediaTypes,
|
||||
login,
|
||||
pingServer,
|
||||
signUpAdmin,
|
||||
uploadFile,
|
||||
} from '@immich/sdk';
|
||||
|
||||
/**
|
||||
* Wraps the underlying API to abstract away the options and make API calls mockable for testing.
|
||||
*/
|
||||
export class ImmichApi {
|
||||
private readonly options;
|
||||
|
||||
constructor(
|
||||
public instanceUrl: string,
|
||||
public apiKey: string,
|
||||
) {
|
||||
this.options = {
|
||||
baseUrl: instanceUrl,
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setApiKey(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
if (!this.options.headers) {
|
||||
throw new Error('missing headers');
|
||||
}
|
||||
this.options.headers['x-api-key'] = apiKey;
|
||||
}
|
||||
|
||||
addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto) {
|
||||
return addAssetsToAlbum({ id, bulkIdsDto }, this.options);
|
||||
}
|
||||
|
||||
checkBulkUpload(assetBulkUploadCheckDto: AssetBulkUploadCheckDto) {
|
||||
return checkBulkUpload({ assetBulkUploadCheckDto }, this.options);
|
||||
}
|
||||
|
||||
createAlbum(createAlbumDto: CreateAlbumDto) {
|
||||
return createAlbum({ createAlbumDto }, this.options);
|
||||
}
|
||||
|
||||
createApiKey(apiKeyCreateDto: ApiKeyCreateDto, options: { headers: { Authorization: string } }) {
|
||||
return createApiKey({ apiKeyCreateDto }, { ...this.options, ...options });
|
||||
}
|
||||
|
||||
getAllAlbums() {
|
||||
return getAllAlbums({}, this.options);
|
||||
}
|
||||
|
||||
getAllAssets() {
|
||||
return getAllAssets({}, this.options);
|
||||
}
|
||||
|
||||
getAssetStatistics() {
|
||||
return getAssetStatistics({}, this.options);
|
||||
}
|
||||
|
||||
getMyUserInfo() {
|
||||
return getMyUserInfo(this.options);
|
||||
}
|
||||
|
||||
getServerVersion() {
|
||||
return getServerVersion(this.options);
|
||||
}
|
||||
|
||||
getSupportedMediaTypes() {
|
||||
return getSupportedMediaTypes(this.options);
|
||||
}
|
||||
|
||||
login(loginCredentialDto: LoginCredentialDto) {
|
||||
return login({ loginCredentialDto }, this.options);
|
||||
}
|
||||
|
||||
pingServer() {
|
||||
return pingServer(this.options);
|
||||
}
|
||||
|
||||
signUpAdmin(signUpDto: SignUpDto) {
|
||||
return signUpAdmin({ signUpDto }, this.options);
|
||||
}
|
||||
|
||||
uploadFile(createAssetDto: CreateAssetDto) {
|
||||
return uploadFile({ createAssetDto }, this.options);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { glob } from 'glob';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
export class CrawlOptions {
|
||||
pathsToCrawl!: string[];
|
||||
recursive? = false;
|
||||
includeHidden? = false;
|
||||
exclusionPatterns?: string[];
|
||||
}
|
||||
|
||||
export class CrawlService {
|
||||
private readonly extensions!: string[];
|
||||
|
||||
constructor(image: string[], video: string[]) {
|
||||
this.extensions = [...image, ...video].map((extension) => extension.replace('.', ''));
|
||||
}
|
||||
|
||||
async crawl(options: CrawlOptions): Promise<string[]> {
|
||||
const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
|
||||
|
||||
if (!pathsToCrawl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const patterns: string[] = [];
|
||||
const crawledFiles: string[] = [];
|
||||
|
||||
for await (const currentPath of pathsToCrawl) {
|
||||
try {
|
||||
const stats = await fs.promises.stat(currentPath);
|
||||
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||
crawledFiles.push(currentPath);
|
||||
} else {
|
||||
patterns.push(currentPath);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
patterns.push(currentPath);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let searchPattern: string;
|
||||
if (patterns.length === 1) {
|
||||
searchPattern = patterns[0];
|
||||
} else if (patterns.length === 0) {
|
||||
return crawledFiles;
|
||||
} else {
|
||||
searchPattern = '{' + patterns.join(',') + '}';
|
||||
}
|
||||
|
||||
if (recursive) {
|
||||
searchPattern = searchPattern + '/**/';
|
||||
}
|
||||
|
||||
searchPattern = `${searchPattern}/*.{${this.extensions.join(',')}}`;
|
||||
|
||||
const globbedFiles = await glob(searchPattern, {
|
||||
absolute: true,
|
||||
nocase: true,
|
||||
nodir: true,
|
||||
dot: includeHidden,
|
||||
ignore: exclusionPatterns,
|
||||
});
|
||||
|
||||
return [...crawledFiles, ...globbedFiles].sort();
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import yaml from 'yaml';
|
||||
import { SessionService } from './session.service';
|
||||
|
||||
const TEST_CONFIG_DIR = '/tmp/immich/';
|
||||
const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
|
||||
const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
|
||||
const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
|
||||
|
||||
const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
const createTestAuthFile = async (contents: string) => {
|
||||
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
||||
// Create config folder if it doesn't exist
|
||||
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(TEST_AUTH_FILE, contents);
|
||||
};
|
||||
|
||||
const readTestAuthFile = async (): Promise<string> => {
|
||||
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
|
||||
};
|
||||
|
||||
const deleteAuthFile = () => {
|
||||
try {
|
||||
fs.unlinkSync(TEST_AUTH_FILE);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })),
|
||||
pingServer: vi.fn(() => Promise.resolve({ res: 'pong' })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./api.service', async (importOriginal) => {
|
||||
const module = await importOriginal<typeof import('./api.service')>();
|
||||
// @ts-expect-error this is only a partial implementation of the return value
|
||||
module.ImmichApi.prototype.getMyUserInfo = mocks.getMyUserInfo;
|
||||
module.ImmichApi.prototype.pingServer = mocks.pingServer;
|
||||
return module;
|
||||
});
|
||||
|
||||
describe('SessionService', () => {
|
||||
let sessionService: SessionService;
|
||||
|
||||
beforeEach(() => {
|
||||
deleteAuthFile();
|
||||
sessionService = new SessionService(TEST_CONFIG_DIR);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deleteAuthFile();
|
||||
});
|
||||
|
||||
it('should connect to immich', async () => {
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
apiKey: TEST_IMMICH_API_KEY,
|
||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
||||
}),
|
||||
);
|
||||
|
||||
await sessionService.connect();
|
||||
expect(mocks.pingServer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should error if no auth file exists', async () => {
|
||||
await sessionService.connect().catch((error) => {
|
||||
expect(error.message).toEqual('No auth file exist. Please login first');
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if auth file is missing instance URl', async () => {
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
apiKey: TEST_IMMICH_API_KEY,
|
||||
}),
|
||||
);
|
||||
await sessionService.connect().catch((error) => {
|
||||
expect(error.message).toEqual(`Instance URL missing in auth config file ${TEST_AUTH_FILE}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if auth file is missing api key', async () => {
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(sessionService.connect()).rejects.toThrow(`API key missing in auth config file ${TEST_AUTH_FILE}`);
|
||||
});
|
||||
|
||||
it('should create auth file when logged in', async () => {
|
||||
await sessionService.login(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY);
|
||||
|
||||
const data: string = await readTestAuthFile();
|
||||
const authConfig = yaml.parse(data);
|
||||
expect(authConfig.instanceUrl).toBe(TEST_IMMICH_INSTANCE_URL);
|
||||
expect(authConfig.apiKey).toBe(TEST_IMMICH_API_KEY);
|
||||
});
|
||||
|
||||
it('should delete auth file when logging out', async () => {
|
||||
const consoleSpy = spyOnConsole();
|
||||
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
apiKey: TEST_IMMICH_API_KEY,
|
||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
||||
}),
|
||||
);
|
||||
await sessionService.logout();
|
||||
|
||||
await fs.promises.access(TEST_AUTH_FILE, fs.constants.F_OK).catch((error) => {
|
||||
expect(error.message).toContain('ENOENT');
|
||||
});
|
||||
|
||||
expect(consoleSpy.mock.calls).toEqual([
|
||||
['Logging out...'],
|
||||
[`Removed auth file ${TEST_AUTH_FILE}`],
|
||||
['Successfully logged out'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import yaml from 'yaml';
|
||||
import { ImmichApi } from './api.service';
|
||||
class LoginError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionService {
|
||||
readonly configDirectory!: string;
|
||||
readonly authPath!: string;
|
||||
|
||||
constructor(configDirectory: string) {
|
||||
this.configDirectory = configDirectory;
|
||||
this.authPath = path.join(configDirectory, '/auth.yml');
|
||||
}
|
||||
|
||||
async connect(): Promise<ImmichApi> {
|
||||
let instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
||||
let apiKey = process.env.IMMICH_API_KEY;
|
||||
|
||||
if (!instanceUrl || !apiKey) {
|
||||
await access(this.authPath, constants.F_OK).catch((error) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new LoginError('No auth file exist. Please login first');
|
||||
}
|
||||
});
|
||||
|
||||
const data: string = await readFile(this.authPath, 'utf8');
|
||||
const parsedConfig = yaml.parse(data);
|
||||
|
||||
instanceUrl = parsedConfig.instanceUrl;
|
||||
apiKey = parsedConfig.apiKey;
|
||||
|
||||
if (!instanceUrl) {
|
||||
throw new LoginError(`Instance URL missing in auth config file ${this.authPath}`);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new LoginError(`API key missing in auth config file ${this.authPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const api = new ImmichApi(instanceUrl, apiKey);
|
||||
|
||||
const pingResponse = await api.pingServer().catch((error) => {
|
||||
throw new Error(`Failed to connect to server ${instanceUrl}: ${error.message}`, error);
|
||||
});
|
||||
|
||||
if (pingResponse.res !== 'pong') {
|
||||
throw new Error(`Could not parse response. Is Immich listening on ${instanceUrl}?`);
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
async login(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
|
||||
console.log('Logging in...');
|
||||
|
||||
const api = new ImmichApi(instanceUrl, apiKey);
|
||||
|
||||
// Check if server and api key are valid
|
||||
const userInfo = await api.getMyUserInfo().catch((error) => {
|
||||
throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
|
||||
});
|
||||
|
||||
console.log(`Logged in as ${userInfo.email}`);
|
||||
|
||||
if (!existsSync(this.configDirectory)) {
|
||||
// Create config folder if it doesn't exist
|
||||
const created = await mkdir(this.configDirectory, { recursive: true });
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create config folder ${this.configDirectory}`);
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 });
|
||||
|
||||
console.log('Wrote auth info to ' + this.authPath);
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
console.log('Logging out...');
|
||||
|
||||
if (existsSync(this.authPath)) {
|
||||
await unlink(this.authPath);
|
||||
console.log('Removed auth file ' + this.authPath);
|
||||
}
|
||||
|
||||
console.log('Successfully logged out');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
import mockfs from 'mock-fs';
|
||||
import { CrawlOptions, CrawlService } from './crawl.service';
|
||||
import { CrawlOptions, crawl } from 'src/utils';
|
||||
|
||||
interface Test {
|
||||
test: string;
|
||||
options: CrawlOptions;
|
||||
options: Omit<CrawlOptions, 'extensions'>;
|
||||
files: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
const extensions = [
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.heif',
|
||||
'.heic',
|
||||
'.tif',
|
||||
'.nef',
|
||||
'.webp',
|
||||
'.tiff',
|
||||
'.dng',
|
||||
'.gif',
|
||||
'.mov',
|
||||
'.mp4',
|
||||
'.webm',
|
||||
];
|
||||
|
||||
const tests: Test[] = [
|
||||
{
|
||||
test: 'should return empty when crawling an empty path list',
|
||||
@@ -251,12 +268,7 @@ const tests: Test[] = [
|
||||
},
|
||||
];
|
||||
|
||||
describe(CrawlService.name, () => {
|
||||
const sut = new CrawlService(
|
||||
['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'],
|
||||
['.mov', '.mp4', '.webm'],
|
||||
);
|
||||
|
||||
describe('crawl', () => {
|
||||
afterEach(() => {
|
||||
mockfs.restore();
|
||||
});
|
||||
@@ -266,7 +278,7 @@ describe(CrawlService.name, () => {
|
||||
it(test, async () => {
|
||||
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)
|
||||
.filter((entry) => entry[1])
|
||||
.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')));
|
||||
});
|
||||
};
|
||||
@@ -15,19 +15,7 @@
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"rootDirs": ["src", "../server/src"],
|
||||
"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"]
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
@@ -14,4 +15,5 @@ export default defineConfig({
|
||||
// bundle everything except for Node built-ins
|
||||
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"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="svg2781" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
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"
|
||||
style="enable-background:new 0 0 564.2 553.5;" xml:space="preserve">
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<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"
|
||||
viewBox="0 0 792 792" style="enable-background:new 0 0 792 792;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4081EF;stroke:#512D8C;stroke-miterlimit:10;}
|
||||
.st1{fill:#31A452;stroke:#512D8C;stroke-miterlimit:10;}
|
||||
.st2{fill:#DE7FB3;stroke:#512D8C;stroke-miterlimit:10;}
|
||||
.st3{fill:#FFB800;stroke:#512D8C;stroke-miterlimit:10;}
|
||||
.st4{fill:#E64132;stroke:#512D8C;stroke-miterlimit:10;}
|
||||
.st5{fill:#F2F5FB;stroke:#512D8C;stroke-miterlimit:10;}
|
||||
.st0{fill:#FA2921;}
|
||||
.st1{fill:#ED79B5;}
|
||||
.st2{fill:#FFB400;}
|
||||
.st3{fill:#1E83F7;}
|
||||
.st4{fill:#18C249;}
|
||||
</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
|
||||
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
|
||||
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
|
||||
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
|
||||
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="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-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
|
||||
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
|
||||
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-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"/>
|
||||
<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
|
||||
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-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
|
||||
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
|
||||
C260.6,438.7,257.9,438.3,255.6,438z"/>
|
||||
<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
|
||||
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
|
||||
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"/>
|
||||
<g id="Flower_00000077325900055813483940000000694823054982625702_">
|
||||
<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
|
||||
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
|
||||
C300.01,209.24,339.15,235.47,375.48,267.63z"/>
|
||||
<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
|
||||
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-18.3,56.34-14.04,130.04,51.53,151.34C162.05,454.77,163.25,455.16,164.7,455.63z"/>
|
||||
<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
|
||||
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
|
||||
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="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-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
|
||||
c0.56-0.76,1.3-1.78,2.19-3.01C363.28,602.32,347.02,558.08,336.54,510.71z"/>
|
||||
<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
|
||||
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
|
||||
c47.92-34.82,87.79-96.95,47.27-152.72C619.2,484.77,618.46,483.75,617.57,482.52z"/>
|
||||
</g>
|
||||
</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/troubleshooting
|
||||
|
||||
version: "3.8"
|
||||
|
||||
name: immich-dev
|
||||
|
||||
x-server-build: &server-common
|
||||
@@ -30,7 +28,7 @@ x-server-build: &server-common
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
|
||||
command: ['/usr/src/app/bin/immich-dev', 'immich']
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 3001:3001
|
||||
@@ -41,7 +39,7 @@ services:
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
|
||||
command: ['/usr/src/app/bin/immich-dev', 'microservices']
|
||||
<<: *server-common
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
@@ -57,7 +55,7 @@ services:
|
||||
image: immich-web-dev:latest
|
||||
build:
|
||||
context: ../web
|
||||
command: [ "/usr/src/app/bin/immich-web" ]
|
||||
command: ['/usr/src/app/bin/immich-web']
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
@@ -99,7 +97,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
@@ -114,6 +112,29 @@ services:
|
||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 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:
|
||||
model-cache:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
name: immich-prod
|
||||
|
||||
x-server-build: &server-common
|
||||
@@ -17,7 +15,7 @@ x-server-build: &server-common
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
command: [ "start.sh", "immich" ]
|
||||
command: ['start.sh', 'immich']
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 2283:3001
|
||||
@@ -27,7 +25,7 @@ services:
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: [ "start.sh", "microservices" ]
|
||||
command: ['start.sh', 'microservices']
|
||||
<<: *server-common
|
||||
# extends:
|
||||
# file: hwaccel.transcoding.yml
|
||||
@@ -56,7 +54,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
restart: always
|
||||
|
||||
database:
|
||||
@@ -73,5 +71,28 @@ services:
|
||||
ports:
|
||||
- 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:
|
||||
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:
|
||||
#
|
||||
@@ -14,7 +12,7 @@ services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
command: ['start.sh', 'immich']
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /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
|
||||
# file: hwaccel.transcoding.yml
|
||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||
command: [ "start.sh", "microservices" ]
|
||||
command: ['start.sh', 'microservices']
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
version: "3.8"
|
||||
|
||||
# Configurations for hardware-accelerated transcoding
|
||||
# Configurations for hardware-accelerated transcoding
|
||||
|
||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||
# you can inline the config for a backend by copying its contents
|
||||
# you can inline the config for a backend by copying its contents
|
||||
# into the immich-microservices service in the docker-compose.yml file.
|
||||
|
||||
# See https://immich.app/docs/features/hardware-transcoding for more info on using hardware transcoding.
|
||||
@@ -38,6 +38,10 @@ services:
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dma_heap:/dev/dma_heap
|
||||
- /dev/mpp_service:/dev/mpp_service
|
||||
#- /dev/mali0:/dev/mali0 # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||
volumes:
|
||||
#- /etc/OpenCL:/etc/OpenCL:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||
#- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro # only required to enable OpenCL-accelerated HDR -> SDR tonemapping
|
||||
|
||||
vaapi:
|
||||
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:
|
||||
|
||||
- [OAuth integration](#livephoto-ios-support-)
|
||||
- [LivePhoto support on iOS](#oauth-integration-)
|
||||
- OAuth integration
|
||||
- LivePhoto support on iOS
|
||||
- User config system
|
||||
|
||||
<!--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?
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
:::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:**
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
- [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/)
|
||||
- [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) |
|
||||
| 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) |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| [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
|
||||
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" />
|
||||
@@ -8,6 +8,8 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
|
||||
| `reset-admin-password` | Reset the password for the admin user |
|
||||
| `disable-password-login` | Disable password login |
|
||||
| `enable-password-login` | Enable password login |
|
||||
| `enable-oauth-login` | Enable OAuth login |
|
||||
| `disable-oauth-login` | Disable OAuth login |
|
||||
| `list-users` | List Immich users |
|
||||
|
||||
## How to run a command
|
||||
@@ -36,13 +38,27 @@ immich-admin disable-password-login
|
||||
Password login has been disabled.
|
||||
```
|
||||
|
||||
Enabled Password Login
|
||||
Enable Password Login
|
||||
|
||||
```
|
||||
immich-admin enable-password-login
|
||||
Password login has been enabled.
|
||||
```
|
||||
|
||||
Enable OAuth login
|
||||
|
||||
```
|
||||
immich-admin enable-oauth-login
|
||||
OAuth login has been enabled.
|
||||
```
|
||||
|
||||
Disable OAuth login
|
||||
|
||||
```
|
||||
immich-admin disable-oauth-login
|
||||
OAuth login has been disabled.
|
||||
```
|
||||
|
||||
List Users
|
||||
|
||||
```
|
||||
|
||||
13
docs/docs/administration/server-stats.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Server Stats
|
||||
|
||||
Server statistics to show the total number of videos, photos, and usage per user.
|
||||
|
||||
:::info
|
||||
If a storage quota has been defined for the user, the usage number will be displayed as a percentage of the total storage quota allocated to him.
|
||||
:::
|
||||
|
||||
:::info External library
|
||||
External library is not included in the storage quota.
|
||||
:::
|
||||
|
||||
<img src={require('./img/server-stats.png').default} title="server statistic" />
|
||||
173
docs/docs/administration/system-settings.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# System Settings
|
||||
|
||||
On the system settings page, the administrator can manage global settings for the Immich instance.
|
||||
|
||||
:::note
|
||||
Viewing and modifying the system settings is restricted to the Administrator.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
You can always return to the default settings by clicking the `Reset to default` button.
|
||||
:::
|
||||
|
||||
## Job Settings
|
||||
|
||||
Using these settings, you can determine the amount of work that will run concurrently for each task in microservices. Some tasks can be set to higher values on computers with powerful hardware and storage with good I/O capabilities.
|
||||
|
||||
With higher concurrency, the host will work on more assets in parallel,
|
||||
this advice improves throughput, not latency, for example, it will make Smart Search jobs process more quickly, but it won't make searching faster.
|
||||
|
||||
It is important to remember that jobs like Smart Search, Face Detection, Facial Recognition, and Transcode Videos require a **lot** of processing power and therefore do not exaggerate the amount of jobs because you're probably thoroughly overloading the server.
|
||||
|
||||
:::info Facial Recognition Concurrency
|
||||
The Facial Recognition Concurrency value cannot be changed because
|
||||
[DBSCAN](https://www.youtube.com/watch?v=RDZUdRSDOok) is traditionally sequential, but there are parallel implementations of it out there. Our implementation isn't parallel.
|
||||
:::
|
||||
|
||||
## External Library
|
||||
|
||||
### Library watching (EXPERIMENTAL)
|
||||
|
||||
External libraries can automatically import changed files without a full rescan. It will import the file whenever the operating system reports a file change. If your photos are mounted over the network, this does not work.
|
||||
|
||||
### Periodic Scanning
|
||||
|
||||
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
|
||||
You can set the scanning interval using the preset or cron format. For more information please refer to e.g. [Crontab Guru](https://crontab.guru/).
|
||||
|
||||
## Logging
|
||||
|
||||
By default logs are set to record at the log level, the network administrator can choose a deeper or lower level of logs according to his decision or according to the needs required by the Immich support team.
|
||||
|
||||
Here you can [learn about the different error levels](https://sematext.com/blog/logging-levels/).
|
||||
|
||||
## Machine Learning Settings
|
||||
|
||||
Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters
|
||||
You can choose to disable a certain type of machine learning, for example smart search or facial recognition.
|
||||
|
||||
### Smart Search
|
||||
|
||||
The smart search settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the
|
||||
Smart Search job on all images to fully apply the change.
|
||||
|
||||
:::info Internet connection
|
||||
Changing models requires a connection to the Internet to download the model.
|
||||
After downloading, there is no need for Immich to connect to the network
|
||||
Unless version checking has been enabled in the settings.
|
||||
:::
|
||||
|
||||
### Facial Recognition
|
||||
|
||||
Under these settings, you can change the facial recognition settings
|
||||
Editable settings:
|
||||
|
||||
- **Facial Recognition Model -** Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model.
|
||||
- **Min Detection Score -** Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.
|
||||
- **Max Recognition Distance -** Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible.
|
||||
- **Min Recognized Faces -** The minimum number of recognized faces for a person to be created (AKA: Core face). Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.
|
||||
|
||||
:::info
|
||||
When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces.
|
||||
You will have to restart **only** the job FACIAL RECOGNITION - ALL.
|
||||
|
||||
If you replace the Facial Recognition Model, you will have to run the job FACE DETECTION - ALL.
|
||||
:::
|
||||
|
||||
:::tip identical twins
|
||||
If you have twins, you might want to lower the Max Recognition Distance value, decreasing this a **bit** can make it distinguish between them.
|
||||
:::
|
||||
|
||||
## Map & GPS Settings
|
||||
|
||||
### Map Settings
|
||||
|
||||
In these settings, you can change the appearance of the map in night and day modes according to your personal preference and according to the supported options.
|
||||
The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for example.
|
||||
|
||||
### Reverse Geocoding Settings
|
||||
|
||||
Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database.
|
||||
|
||||
## OAuth Authentication
|
||||
|
||||
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
|
||||
|
||||
## Password Authentication
|
||||
|
||||
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
|
||||
|
||||
:::tip
|
||||
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
|
||||
:::
|
||||
|
||||
## Server Settings
|
||||
|
||||
### External Domain
|
||||
|
||||
When set, will override the domain name used when viewing and copying a shared link.
|
||||
|
||||
### Welcome Message
|
||||
|
||||
The administrator can set a custom message on the login screen (the message will be displayed to all users).
|
||||
|
||||
## Storage Template
|
||||
|
||||
Immich supports a custom [Storage Template](/docs/administration/storage-template). Learn more about this feature and its configuration [here](/docs/administration/storage-template).
|
||||
|
||||
## Theme Settings
|
||||
|
||||
You can write custom CSS that will get loaded in the web application for all users. This enables administrators to change fonts, colors, and other styles.
|
||||
|
||||
For example:
|
||||
|
||||
```css title='Custom CSS'
|
||||
p {
|
||||
color: green;
|
||||
}
|
||||
```
|
||||
|
||||
## Thumbnail Settings
|
||||
|
||||
By default Immich creates 3 thumbnails for each asset,
|
||||
Blurred (thumbhash) , Small (webp) , and Large (jpeg), using these settings you can change the quality for the thumbnail files that are created.
|
||||
|
||||
**Small thumbnail resolution**
|
||||
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
|
||||
|
||||
**Large thumbnail resolution**
|
||||
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
|
||||
|
||||
**Quality**
|
||||
Thumbnail quality from 1-100. Higher is better for quality but produces larger files.
|
||||
|
||||
**Prefer wide gamut**
|
||||
Use display p3 for thumbnails. This better preserves the vibrance of images with wide color spaces, but images may appear differently on old devices with an old browser version. Srgb images are kept as srgb to avoid color shifts.
|
||||
|
||||
:::tip
|
||||
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
|
||||
:::
|
||||
|
||||
## Trash Settings
|
||||
|
||||
In the system administrator's option to set a trash for deleted files, these files will remain in the trash until the deletion date 30 days (default) or as defined by the system administrator.
|
||||
|
||||
The trash can be disabled, however this is not recommended as future files that are deleted will be permanently deleted.
|
||||
|
||||
:::tip Keyboard shortcut for permanently deletion
|
||||
You can select assets and press Ctrl + Del from the timeline for quick permanent deletion without the trash option.
|
||||
:::
|
||||
|
||||
## User Settings
|
||||
|
||||
### Delete delay
|
||||
|
||||
The system administrator can choose to delete users through the administration panel, the system administrator can delete users immediately or alternatively delay the deletion for users (7 days by default) this action permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.
|
||||
|
||||
## Version Check
|
||||
|
||||
When this option is enabled the `immich-server` will periodically make requests to GitHub to check for new releases.
|
||||
|
||||
## Video Transcoding Settings
|
||||
|
||||
The system administrator can define parameters according to which video files will be converted to different formats (depending on the settings). The settings can be changed in depth, to learn more about the terminology used here, refer to FFmpeg documentation for [H.264](https://trac.ffmpeg.org/wiki/Encode/H.264) codec, [HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) codec and [VP9](https://trac.ffmpeg.org/wiki/Encode/VP9) codec.
|
||||
@@ -13,12 +13,57 @@ Immich supports multiple users, each with their own library.
|
||||
|
||||
<UserCreate />
|
||||
|
||||
## Delete a User
|
||||
## Set Storage Quota For User
|
||||
|
||||
If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days.
|
||||
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
|
||||
|
||||
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota using the value 0 (default).
|
||||
|
||||
:::tip
|
||||
The system administrator can see the usage quota percentage of all users in Server Stats page.
|
||||
:::
|
||||
|
||||
:::info
|
||||
External libraries don't take up space from the storage quota.
|
||||
:::
|
||||
|
||||
<img src={require('./img/user-quota-size.png').default} width="40%" title="Set Quota Size" />
|
||||
|
||||
## Set Storage Label For User
|
||||
|
||||
The admin can add a custom label for each user, so instead of `upload/{userId}/your-template` it will be `upload/{custom_user_label}/your-template`.
|
||||
To apply a storage template, go to the Administration page -> click on the pencil button next to the user.
|
||||
:::note
|
||||
To apply the Storage Label to previously uploaded assets, run the Storage Migration Job.
|
||||
:::
|
||||
|
||||
<img src={require('./img/user-storage-label.png').default} width="40%" title="Delete User" />
|
||||
|
||||
## Password Reset
|
||||
|
||||
To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to "password" and they have to change it next time the sign in.
|
||||
To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to random password and they have to change it next time the sign in.
|
||||
|
||||

|
||||
<img src={require('./img/user-management-update.png').default} width="40%" title="Reset Password" />
|
||||
|
||||
## Delete a User
|
||||
|
||||
If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default.
|
||||
|
||||
<img src={require('./img/delete-user.webp').default} width="40%" title="Delete User" />
|
||||
|
||||
### Delete Delay
|
||||
|
||||
You can customize the time of the deletion of the users from the Administration -> Settings -> User Settings.
|
||||
:::info user deletion job
|
||||
The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.
|
||||
:::
|
||||
|
||||
<img src={require('./img/customize-delete-user.png').default} width="80%" title="Customize Delete User" />
|
||||
|
||||
### Immediately Remove User
|
||||
|
||||
You can choose to delete a user immediately by checking the box
|
||||
`Queue user and assets for immediate deletion` in the deletion process, this will immediately remove the user and all assets.
|
||||
This cannot be undone and the files cannot be recovered.
|
||||
|
||||
<img src={require('./img/immediately-remove-user.png').default} width="40%" title="Customize Delete User" />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Database Migrations
|
||||
|
||||
After making any changes in the `server/src/infra/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
|
||||
After making any changes in the `server/src/entities`, a database migration need to run in order to register the changes in the database. Follow the steps below to create a new migration.
|
||||
|
||||
1. Run the command
|
||||
|
||||
```bash
|
||||
npm run typeorm:migrations:generate ./src/infra/<migration-name>
|
||||
npm run typeorm:migrations:generate <migration-name>
|
||||
```
|
||||
|
||||
2. Check if the migration file makes sense.
|
||||
3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
|
||||
3. Move the migration file to folder `./server/src/migrations` in your code editor.
|
||||
|
||||
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
||||
|
||||
@@ -10,7 +10,7 @@ If foreground backup is enabled: whenever the app is opened or resumed, it will
|
||||
|
||||
## Background backup
|
||||
|
||||
Background backup is available thanks to the contribution effort of [@zoodyy](https://github.com/zoodyy) and [@martyfuhry](https://github.com/martyfuhry).
|
||||
Background backup is available thanks to the contribution effort of [@fyfrey](https://github.com/fyfrey) and [@martyfuhry](https://github.com/martyfuhry).
|
||||
|
||||
If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the cloud. If there are, it will upload them to the cloud in the background.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Immich CLI
|
||||
|
||||
Immich has a CLI that allows you to perform certain actions from the command line. This CLI replaces the [legacy CLI](https://github.com/immich-app/CLI) that was previously available. The CLI is hosted in the [cli folder of the the main Immich github repository](https://github.com/immich-app/immich/tree/main/cli).
|
||||
Immich has a command line interface (CLI) that allows you to perform certain actions from the command line.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -54,16 +54,19 @@ Usage: immich [options] [command]
|
||||
Command line interface for Immich
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
-d, --config Configuration directory (env: IMMICH_CONFIG_DIR)
|
||||
-h, --help display help for command
|
||||
-V, --version output the version number
|
||||
-d, --config-directory <directory> Configuration directory where auth.yml will be stored (default: "~/.config/immich/", env:
|
||||
IMMICH_CONFIG_DIR)
|
||||
-u, --url [url] Immich server URL (env: IMMICH_INSTANCE_URL)
|
||||
-k, --key [key] Immich API key (env: IMMICH_API_KEY)
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
upload [options] [paths...] Upload assets
|
||||
server-info Display server information
|
||||
login-key [instanceUrl] [apiKey] Login using an API key
|
||||
logout Remove stored credentials
|
||||
help [command] display help for command
|
||||
login|login-key <url> <key> Login using an API key
|
||||
logout Remove stored credentials
|
||||
server-info Display server information
|
||||
upload [options] [paths...] Upload assets
|
||||
help [command] display help for command
|
||||
```
|
||||
|
||||
## Commands
|
||||
@@ -71,23 +74,24 @@ Commands:
|
||||
The upload command supports the following options:
|
||||
|
||||
```
|
||||
Usage: immich upload [options] [paths...]
|
||||
Usage: immich upload [paths...] [options]
|
||||
|
||||
Upload assets
|
||||
|
||||
Arguments:
|
||||
paths One or more paths to assets to be uploaded
|
||||
paths One or more paths to assets to be uploaded
|
||||
|
||||
Options:
|
||||
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
||||
-i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS)
|
||||
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
||||
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
||||
--help display help for command
|
||||
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
||||
-i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS)
|
||||
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
||||
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
|
||||
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
||||
--help display help for command
|
||||
```
|
||||
|
||||
Note that the above options can read from environment variables as well.
|
||||
@@ -97,13 +101,13 @@ Note that the above options can read from environment variables as well.
|
||||
You begin by authenticating to your Immich server.
|
||||
|
||||
```bash
|
||||
immich login-key [instanceUrl] [apiKey]
|
||||
immich login [url] [key]
|
||||
```
|
||||
|
||||
For instance,
|
||||
|
||||
```bash
|
||||
immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG
|
||||
immich login http://192.168.1.216:2283/api HFEJ38DNSDUEG
|
||||
```
|
||||
|
||||
This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
|
||||
|
||||
@@ -42,6 +42,18 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
|
||||
- If you have an 11th gen CPU or older, then you may need to follow [these][jellyfin-lp] instructions as Low-Power mode is required
|
||||
- Additionally, if the server specifically has an 11th gen CPU and is running kernel 5.15 (shipped with Ubuntu 22.04 LTS), then you will need to upgrade this kernel (from [Jellyfin docs][jellyfin-kernel-bug])
|
||||
|
||||
#### RKMPP
|
||||
|
||||
For RKMPP to work:
|
||||
|
||||
- You must have a supported Rockchip ARM SoC.
|
||||
- Only RK3588 supports hardware tonemapping, other SoCs use slower software tonemapping while still using hardware encoding.
|
||||
- Tonemapping requires `/usr/lib/aarch64-linux-gnu/libmali.so.1` to be present on your host system. Install [`libmali-valhall-g610-g6p0-gbm`][libmali-rockchip] and modify the [`hwaccel.transcoding.yml`][hw-file] file:
|
||||
- under `rkmpp` uncomment the 3 lines required for OpenCL tonemapping by removing the `#` symbol at the beginning of each line
|
||||
- `- /dev/mali0:/dev/mali0`
|
||||
- `- /etc/OpenCL:/etc/OpenCL:ro`
|
||||
- `- /usr/lib/aarch64-linux-gnu/libmali.so.1:/usr/lib/aarch64-linux-gnu/libmali.so.1:ro`
|
||||
|
||||
## Setup
|
||||
|
||||
#### Basic Setup
|
||||
@@ -106,3 +118,4 @@ Once this is done, you can continue to step 3 of "Basic Setup".
|
||||
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
|
||||
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
|
||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
||||
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
|
||||
|
||||