Compare commits
291 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 388e132fca | |||
| 43e0a59f93 | |||
| da34bd714e | |||
| 77da10e3ab | |||
| 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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Update Immich SDK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-sdk-repos:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'immich-app',
|
||||
repo: 'immich-sdk-typescript-axios',
|
||||
workflow_id: 'build.yml',
|
||||
ref: 'main'
|
||||
})
|
||||
@@ -66,13 +66,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- 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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Update Immich SDK
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish `@immich/sdk`
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Publish
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.16.9"
|
||||
flutter-version: "3.19.3"
|
||||
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
|
||||
@@ -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
|
||||
@@ -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() }}
|
||||
@@ -213,6 +192,10 @@ jobs:
|
||||
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 chromium
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -346,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@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: |
|
||||
@@ -371,7 +354,7 @@ jobs:
|
||||
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'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.0.8",
|
||||
"version": "2.1.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"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { AssetBulkUploadCheckResult } from '@immich/sdk';
|
||||
import {
|
||||
AssetBulkUploadCheckResult,
|
||||
addAssetsToAlbum,
|
||||
checkBulkUpload,
|
||||
createAlbum,
|
||||
defaults,
|
||||
getAllAlbums,
|
||||
getSupportedMediaTypes,
|
||||
} from '@immich/sdk';
|
||||
import byteSize from 'byte-size';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { chunk, zip } from 'lodash-es';
|
||||
@@ -7,9 +15,8 @@ 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';
|
||||
import { CrawlService } from 'src/services/crawl.service';
|
||||
import { BaseOptions, authenticate } from 'src/utils';
|
||||
|
||||
const zipDefined = zip as <T, U>(a: T[], b: U[]) => [T, U][];
|
||||
|
||||
@@ -66,8 +73,8 @@ class Asset {
|
||||
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
||||
deviceAssetId: this.deviceAssetId,
|
||||
deviceId: 'CLI',
|
||||
fileCreatedAt: this.fileCreatedAt,
|
||||
fileModifiedAt: this.fileModifiedAt,
|
||||
fileCreatedAt: this.fileCreatedAt.toISOString(),
|
||||
fileModifiedAt: this.fileModifiedAt.toISOString(),
|
||||
isFavorite: String(false),
|
||||
};
|
||||
const formData = new FormData();
|
||||
@@ -106,7 +113,7 @@ class Asset {
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadOptionsDto {
|
||||
class UploadOptionsDto {
|
||||
recursive? = false;
|
||||
exclusionPatterns?: string[] = [];
|
||||
dryRun? = false;
|
||||
@@ -118,11 +125,13 @@ export class UploadOptionsDto {
|
||||
concurrency? = 4;
|
||||
}
|
||||
|
||||
export class UploadCommand extends BaseCommand {
|
||||
api!: ImmichApi;
|
||||
export const upload = (paths: string[], baseOptions: BaseOptions, uploadOptions: UploadOptionsDto) =>
|
||||
new UploadCommand().run(paths, baseOptions, uploadOptions);
|
||||
|
||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||
this.api = await this.connect();
|
||||
// TODO refactor this
|
||||
class UploadCommand {
|
||||
public async run(paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto): Promise<void> {
|
||||
await authenticate(baseOptions);
|
||||
|
||||
console.log('Crawling for assets...');
|
||||
const files = await this.getFiles(paths, options);
|
||||
@@ -264,7 +273,7 @@ export class UploadCommand extends BaseCommand {
|
||||
}
|
||||
|
||||
public async getAlbums(): Promise<Map<string, string>> {
|
||||
const existingAlbums = await this.api.getAllAlbums();
|
||||
const existingAlbums = await getAllAlbums({});
|
||||
|
||||
const albumMapping = new Map<string, string>();
|
||||
for (const album of existingAlbums) {
|
||||
@@ -313,7 +322,7 @@ export class UploadCommand extends BaseCommand {
|
||||
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)),
|
||||
albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } }).then((r) => r.id)),
|
||||
);
|
||||
|
||||
for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) {
|
||||
@@ -348,7 +357,7 @@ export class UploadCommand extends BaseCommand {
|
||||
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 });
|
||||
await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } });
|
||||
albumUpdateProgress.increment(assetBatch.length);
|
||||
}
|
||||
}
|
||||
@@ -404,17 +413,18 @@ export class UploadCommand extends BaseCommand {
|
||||
const assetBulkUploadCheckDto = {
|
||||
assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })),
|
||||
};
|
||||
const checkResponse = await this.api.checkBulkUpload(assetBulkUploadCheckDto);
|
||||
const checkResponse = await 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)));
|
||||
const results = await Promise.all(fileRequests.map((request) => this.uploadAsset(request)));
|
||||
return results.map((response) => response.id);
|
||||
}
|
||||
|
||||
private async crawl(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
||||
const formatResponse = await this.api.getSupportedMediaTypes();
|
||||
const formatResponse = await getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
||||
|
||||
return crawlService.crawl({
|
||||
@@ -426,14 +436,12 @@ export class UploadCommand extends BaseCommand {
|
||||
}
|
||||
|
||||
private async uploadAsset(data: FormData): Promise<{ id: string }> {
|
||||
const url = this.api.instanceUrl + '/asset/upload';
|
||||
const { baseUrl, headers } = defaults;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await fetch(`${baseUrl}/asset/upload`, {
|
||||
method: 'post',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'x-api-key': this.api.apiKey,
|
||||
},
|
||||
headers: headers as Record<string, string>,
|
||||
body: data,
|
||||
});
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
@@ -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 (instanceUrl: string, apiKey: string, options: BaseOptions) => {
|
||||
console.log(`Logging in to ${instanceUrl}`);
|
||||
|
||||
const { configDirectory: configDir } = options;
|
||||
|
||||
await connect(instanceUrl, apiKey);
|
||||
|
||||
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, { instanceUrl, apiKey });
|
||||
|
||||
console.log(`Wrote auth info to ${getAuthFilePath(configDir)}`);
|
||||
};
|
||||
|
||||
export const logout = async (options: BaseOptions) => {
|
||||
console.log('Logging out...');
|
||||
|
||||
const { configDirectory: configDir } = options;
|
||||
|
||||
const authFile = getAuthFilePath(configDir);
|
||||
|
||||
if (existsSync(authFile)) {
|
||||
await unlink(authFile);
|
||||
console.log(`Removed auth file: ${authFile}`);
|
||||
}
|
||||
|
||||
console.log('Successfully logged out');
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { ImmichApi } from 'src/services/api.service';
|
||||
import { SessionService } from '../services/session.service';
|
||||
|
||||
export abstract class BaseCommand {
|
||||
protected sessionService!: SessionService;
|
||||
protected user!: UserResponseDto;
|
||||
protected serverVersion!: ServerVersionResponseDto;
|
||||
|
||||
constructor(options: { configDirectory?: string }) {
|
||||
if (!options.configDirectory) {
|
||||
throw new Error('Config directory is required');
|
||||
}
|
||||
this.sessionService = new SessionService(options.configDirectory);
|
||||
}
|
||||
|
||||
public async connect(): Promise<ImmichApi> {
|
||||
return await this.sessionService.connect();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
export class LoginCommand extends BaseCommand {
|
||||
public async run(instanceUrl: string, apiKey: string): Promise<void> {
|
||||
await this.sessionService.login(instanceUrl, apiKey);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
export class LogoutCommand extends BaseCommand {
|
||||
public static readonly description = 'Logout and remove persisted credentials';
|
||||
public async run(): Promise<void> {
|
||||
await this.sessionService.logout();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
export class ServerInfoCommand extends BaseCommand {
|
||||
public async run() {
|
||||
const api = await this.connect();
|
||||
const versionInfo = await api.getServerVersion();
|
||||
const mediaTypes = await api.getSupportedMediaTypes();
|
||||
const statistics = await api.getAssetStatistics();
|
||||
|
||||
console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
||||
console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
|
||||
console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`);
|
||||
console.log(
|
||||
`Statistics:\n Images: ${statistics.images}\n Videos: ${statistics.videos}\n Total: ${statistics.total}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getAssetStatistics, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
||||
import { BaseOptions, authenticate } from 'src/utils';
|
||||
|
||||
export const serverInfo = async (options: BaseOptions) => {
|
||||
await authenticate(options);
|
||||
|
||||
const versionInfo = await getServerVersion();
|
||||
const mediaTypes = await getSupportedMediaTypes();
|
||||
const stats = await 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: ${stats.images}\n Videos: ${stats.videos}\n Total: ${stats.total}`);
|
||||
};
|
||||
@@ -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 [apiKey]', '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,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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import yaml from 'yaml';
|
||||
|
||||
export interface BaseOptions {
|
||||
configDirectory: string;
|
||||
apiKey?: string;
|
||||
instanceUrl?: string;
|
||||
}
|
||||
|
||||
export interface AuthDto {
|
||||
instanceUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export const authenticate = async (options: BaseOptions): Promise<void> => {
|
||||
const { configDirectory: configDir, instanceUrl, apiKey } = options;
|
||||
|
||||
// provided in command
|
||||
if (instanceUrl && apiKey) {
|
||||
await connect(instanceUrl, apiKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback to file
|
||||
const config = await readAuthFile(configDir);
|
||||
await connect(config.instanceUrl, config.apiKey);
|
||||
};
|
||||
|
||||
export const connect = async (instanceUrl: string, apiKey: string): Promise<void> => {
|
||||
const wellKnownUrl = new URL('.well-known/immich', instanceUrl);
|
||||
try {
|
||||
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
||||
const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString();
|
||||
if (endpoint !== instanceUrl) {
|
||||
console.debug(`Discovered API at ${endpoint}`);
|
||||
}
|
||||
instanceUrl = endpoint;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
defaults.baseUrl = instanceUrl;
|
||||
defaults.headers = { 'x-api-key': apiKey };
|
||||
|
||||
const [error] = await withError(getMyUserInfo());
|
||||
if (isHttpError(error)) {
|
||||
logError(error, 'Failed to connect to server');
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
return yaml.parse(data.toString()) 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];
|
||||
}
|
||||
};
|
||||
@@ -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 |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Router_Medium_x5F_Black_00000159464448132936669960000002337362428709113490_"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 266.25"
|
||||
style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#ACCBFA;}
|
||||
.st1{fill:#FA2921;}
|
||||
.st2{fill:#ED79B5;}
|
||||
.st3{fill:#FFB400;}
|
||||
.st4{fill:#1E83F7;}
|
||||
.st5{fill:#18C249;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||
<path class="st0" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||
<path class="st0" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||
<path class="st0" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||
<path class="st2" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||
<path class="st3" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||
<path class="st4" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||
<path class="st5" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Router_Medium_x5F_White" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 792 266.25" style="enable-background:new 0 0 792 266.25;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4251B0;}
|
||||
.st1{fill:#FA2921;}
|
||||
.st2{fill:#ED79B5;}
|
||||
.st3{fill:#FFB400;}
|
||||
.st4{fill:#1E83F7;}
|
||||
.st5{fill:#18C249;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
c5.84,0,10.52,4.67,10.52,10.68c0,2.84-0.83,7.68-0.83,10.68v38.73c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" d="M394.28,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.83-7.68,0.83-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C399.12,182.55,394.28,177.88,394.28,171.87z"/>
|
||||
<path class="st0" d="M528.5,171.87c0-2.84,0.83-7.68,0.83-10.68V132.3c0-10.18-5.34-16.86-14.52-16.86c-6.01,0-11.35,3-14.86,8.85
|
||||
c0.33,1.84,0.5,3.67,0.5,5.68v31.22c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.68,10.68c-5.51,0-10.35-4.67-10.35-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68V131.8c0-3.17-0.5-6.01-1.67-8.51c-2.17-4.84-6.51-7.85-12.52-7.85c-6.18,0-11.19,3.17-14.86,8.85
|
||||
v36.9c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68c-5.84,0-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68
|
||||
v-38.57c0-3.01-1.5-8.35-1.5-10.85c0-6.01,4.34-10.68,10.18-10.68c5.51,0,8.68,3.67,9.68,8.51c5.01-6.68,12.02-10.85,21.2-10.85
|
||||
c10.85,0,18.7,5.18,23.54,13.52c5.51-8.68,13.52-13.52,23.54-13.52c16.86,0,29.72,12.19,29.72,31.72v30.72
|
||||
c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.51,10.68-10.52,10.68C533.35,182.55,528.5,177.88,528.5,171.87z"/>
|
||||
<path class="st0" d="M576.92,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C565.23,68.36,570.57,63.18,576.92,63.18z M567.07,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
s10.52,4.67,10.52,10.68c0,2.84-0.84,7.68-0.84,10.68v38.73c0,3.01,0.84,7.85,0.84,10.68c0,6.01-4.67,10.68-10.52,10.68
|
||||
s-10.52-4.67-10.52-10.68c0-2.84,0.67-7.68,0.67-10.68V122.45z"/>
|
||||
<path class="st0" d="M601.79,141.31c0-23.54,14.69-42.57,39.07-42.57c12.86,0,24.71,5.84,30.05,14.53c2,3.17,2.34,5.01,2.34,6.51
|
||||
c0,5.18-4.01,9.52-9.85,9.52c-3.84,0-7.34-2.17-8.85-6.01c-2.34-5.18-6.85-8.18-13.69-8.18c-12.86,0-20.03,11.52-20.03,26.04
|
||||
c0,14.69,7.51,26.04,20.53,26.04c7.01,0,12.02-2.5,14.36-7.68c1.67-3.51,4.84-6.51,9.18-6.51c6.01,0,9.68,4.17,9.68,9.35
|
||||
c0,2.5-1,5.51-3.17,8.35c-5.51,7.35-15.86,13.19-30.05,13.19C616.32,183.89,601.79,165.19,601.79,141.31z"/>
|
||||
<path class="st0" d="M737.69,171.87c0-2.84,0.67-7.68,0.67-10.68v-28.55c0-10.18-5.68-17.2-15.36-17.2
|
||||
c-6.68,0-12.35,3.17-16.03,8.35v37.4c0,3.01,0.67,7.85,0.67,10.68c0,6.01-4.67,10.68-10.52,10.68s-10.52-4.67-10.52-10.68
|
||||
c0-2.84,0.84-7.68,0.84-10.68v-80.8c0-3.01-0.84-7.85-0.84-10.68c0-6.01,4.84-10.68,10.52-10.68c5.84,0,10.52,4.67,10.52,10.68
|
||||
c0,2.84-0.67,7.68-0.67,10.68v27.21c5.01-5.51,12.19-8.85,21.37-8.85c17.2,0,29.55,12.86,29.55,31.22v31.22
|
||||
c0,3.01,0.83,7.85,0.83,10.68c0,6.01-4.84,10.68-10.52,10.68C742.36,182.55,737.69,177.88,737.69,171.87z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M114.82,96.21c11.92,10.55,21.52,21.86,27.7,32.52c10.62-18.99,17.71-41.55,17.8-55.92c0-0.1,0-0.19,0-0.28
|
||||
c0-21.26-21.21-29.54-39.48-29.54s-39.48,8.28-39.48,29.54c0,0.29,0,0.68,0,1.15C91.54,78.2,103.61,86.29,114.82,96.21z"/>
|
||||
<path class="st2" d="M49.8,154.19c7.45-8.29,18.88-17.27,31.77-24.86c13.72-8.07,27.44-13.71,39.49-16.3
|
||||
c-14.78-15.96-34.04-29.68-47.68-34.21c-0.1-0.03-0.18-0.06-0.27-0.09c-20.22-6.57-34.65,11.05-40.3,28.42s-4.33,40.11,15.89,46.68
|
||||
C48.99,153.93,49.35,154.05,49.8,154.19z"/>
|
||||
<path class="st3" d="M209.07,106.86c-5.65-17.38-20.07-34.99-40.3-28.42c-0.28,0.09-0.65,0.21-1.09,0.35
|
||||
c-1.16,11.08-5.12,25.07-11.09,38.79c-6.35,14.6-14.14,27.23-22.36,36.39c21.34,4.23,44.99,4,58.68-0.35
|
||||
c0.1-0.03,0.19-0.06,0.27-0.09C213.4,146.97,214.71,124.24,209.07,106.86z"/>
|
||||
<path class="st4" d="M102.8,171.18c-3.44-15.54-4.56-30.34-3.3-42.59c-19.75,9.12-38.75,23.2-47.27,34.78
|
||||
c-0.06,0.08-0.11,0.16-0.16,0.23c-12.5,17.2-0.2,36.37,14.58,47.11s36.81,16.51,49.31-0.69c0.17-0.24,0.4-0.55,0.68-0.93
|
||||
C111.05,199.44,106.04,185.79,102.8,171.18z"/>
|
||||
<path class="st5" d="M189.48,162.49c-10.9,2.33-25.42,2.88-40.32,1.44c-15.84-1.53-30.26-5.03-41.52-10.02
|
||||
c2.57,21.6,10.09,44.02,18.47,55.7c0.06,0.08,0.11,0.16,0.16,0.23c12.5,17.2,34.52,11.43,49.31,0.69
|
||||
c14.78-10.74,27.08-29.9,14.58-47.11C189.99,163.18,189.76,162.86,189.48,162.49z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Router_Medium_x5F_Black_00000037681990313894948460000012967653829507626171_"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 792"
|
||||
style="enable-background:new 0 0 792 792;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#ACCBFA;}
|
||||
.st1{fill:#FA2921;}
|
||||
.st2{fill:#ED79B5;}
|
||||
.st3{fill:#FFB400;}
|
||||
.st4{fill:#1E83F7;}
|
||||
.st5{fill:#18C249;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M110.16,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||
C95.71,543.8,102.32,537.4,110.16,537.4z M97.98,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||
<path class="st0" d="M265.44,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||
c-6.81,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.69-5.99-8.05-9.71-15.49-9.71
|
||||
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.81,0,10.74,4.54,11.98,10.53
|
||||
c6.19-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.73-16.73,29.11-16.73
|
||||
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||
C271.43,685.04,265.44,679.26,265.44,671.82z"/>
|
||||
<path class="st0" d="M431.45,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||
c-6.82,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.68-5.99-8.05-9.71-15.49-9.71
|
||||
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.82,0,10.74,4.54,11.98,10.53
|
||||
c6.2-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.72-16.73,29.11-16.73
|
||||
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||
C437.44,685.04,431.45,679.26,431.45,671.82z"/>
|
||||
<path class="st0" d="M491.33,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||
C476.87,543.8,483.48,537.4,491.33,537.4z M479.15,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||
<path class="st0" d="M522.09,634.04c0-29.11,18.17-52.65,48.32-52.65c15.9,0,30.56,7.23,37.17,17.97c2.48,3.92,2.89,6.19,2.89,8.05
|
||||
c0,6.4-4.96,11.77-12.18,11.77c-4.75,0-9.08-2.68-10.94-7.43c-2.89-6.4-8.47-10.12-16.93-10.12c-15.9,0-24.78,14.25-24.78,32.21
|
||||
c0,18.17,9.29,32.21,25.4,32.21c8.67,0,14.87-3.1,17.76-9.5c2.06-4.34,5.99-8.05,11.36-8.05c7.43,0,11.98,5.16,11.98,11.56
|
||||
c0,3.1-1.24,6.81-3.92,10.32c-6.82,9.09-19.62,16.31-37.17,16.31C540.06,686.69,522.09,663.56,522.09,634.04z"/>
|
||||
<path class="st0" d="M690.17,671.82c0-3.51,0.83-9.5,0.83-13.22v-35.3c0-12.6-7.02-21.27-19-21.27c-8.26,0-15.28,3.92-19.82,10.32
|
||||
v46.25c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.78,13.22-13.01,13.22s-13.01-5.78-13.01-13.22c0-3.51,1.03-9.5,1.03-13.22v-99.94
|
||||
c0-3.72-1.03-9.71-1.03-13.22c0-7.43,5.99-13.22,13.01-13.22c7.23,0,13.01,5.78,13.01,13.22c0,3.51-0.83,9.5-0.83,13.22v33.66
|
||||
c6.2-6.81,15.07-10.94,26.43-10.94c21.27,0,36.55,15.9,36.55,38.61v38.61c0,3.72,1.03,9.71,1.03,13.22
|
||||
c0,7.43-5.99,13.22-13.01,13.22C695.95,685.04,690.17,679.26,690.17,671.82z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M376.76,216.42c28.32,25.07,51.15,51.95,65.83,77.27c25.23-45.12,42.08-98.73,42.3-132.88
|
||||
c0-0.24,0-0.46,0-0.66c0-50.53-50.41-70.2-93.82-70.2s-93.82,19.66-93.82,70.2c0,0.69,0,1.62,0,2.73
|
||||
C321.44,173.62,350.14,192.84,376.76,216.42z"/>
|
||||
<path class="st2" d="M222.27,354.21c17.7-19.69,44.85-41.04,75.5-59.08c32.6-19.19,65.21-32.59,93.83-38.73
|
||||
c-35.11-37.94-80.89-70.53-113.31-81.29c-0.23-0.07-0.44-0.14-0.63-0.21c-48.06-15.61-82.34,26.25-95.75,67.54
|
||||
c-13.42,41.29-10.29,95.31,37.77,110.92C220.33,353.58,221.21,353.86,222.27,354.21z"/>
|
||||
<path class="st3" d="M600.73,241.74c-13.42-41.29-47.69-83.15-95.75-67.54c-0.66,0.21-1.54,0.5-2.6,0.84
|
||||
c-2.75,26.34-12.16,59.57-26.36,92.17c-15.09,34.68-33.6,64.69-53.14,86.48c50.7,10.05,106.9,9.52,139.45-0.83
|
||||
c0.23-0.07,0.44-0.14,0.63-0.21C611.02,337.05,614.15,283.03,600.73,241.74z"/>
|
||||
<path class="st4" d="M348.22,394.58c-8.17-36.93-10.84-72.09-7.84-101.2c-46.93,21.67-92.08,55.14-112.33,82.64
|
||||
c-0.14,0.19-0.27,0.37-0.39,0.54c-29.7,40.88-0.48,86.42,34.64,111.94s87.46,39.24,117.16-1.64c0.41-0.56,0.95-1.31,1.6-2.21
|
||||
C367.81,461.72,355.9,429.3,348.22,394.58z"/>
|
||||
<path class="st5" d="M554.19,373.91c-25.9,5.53-60.41,6.84-95.81,3.42c-37.65-3.64-71.91-11.96-98.67-23.82
|
||||
c6.11,51.33,23.99,104.61,43.89,132.37c0.14,0.19,0.27,0.37,0.39,0.54c29.7,40.88,82.04,27.16,117.16,1.64S585.5,417,555.8,376.12
|
||||
C555.39,375.56,554.85,374.81,554.19,373.91z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Router_Medium_x5F_White_00000062189486027058041470000012691761407447023025_"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 792 792"
|
||||
style="enable-background:new 0 0 792 792;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4251B0;}
|
||||
.st1{fill:#FA2921;}
|
||||
.st2{fill:#ED79B5;}
|
||||
.st3{fill:#FFB400;}
|
||||
.st4{fill:#1E83F7;}
|
||||
.st5{fill:#18C249;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M110.16,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||
C95.71,543.8,102.32,537.4,110.16,537.4z M97.98,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||
<path class="st0" d="M265.44,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||
c-6.81,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.69-5.99-8.05-9.71-15.49-9.71
|
||||
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.81,0,10.74,4.54,11.98,10.53
|
||||
c6.19-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.73-16.73,29.11-16.73
|
||||
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||
C271.43,685.04,265.44,679.26,265.44,671.82z"/>
|
||||
<path class="st0" d="M431.45,671.82c0-3.51,1.03-9.5,1.03-13.22v-35.72c0-12.6-6.61-20.85-17.96-20.85
|
||||
c-7.43,0-14.04,3.72-18.38,10.94c0.41,2.27,0.62,4.54,0.62,7.02v38.61c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.22,13.22
|
||||
c-6.82,0-12.8-5.78-12.8-13.22c0-3.51,1.03-9.5,1.03-13.22v-36.34c0-3.92-0.62-7.43-2.06-10.53c-2.68-5.99-8.05-9.71-15.49-9.71
|
||||
c-7.64,0-13.83,3.92-18.38,10.94v45.63c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.99,13.22-13.01,13.22c-7.23,0-13.01-5.78-13.01-13.22
|
||||
c0-3.51,0.83-9.5,0.83-13.22v-47.7c0-3.72-1.86-10.32-1.86-13.42c0-7.43,5.37-13.22,12.6-13.22c6.82,0,10.74,4.54,11.98,10.53
|
||||
c6.2-8.26,14.87-13.42,26.22-13.42c13.42,0,23.13,6.4,29.11,16.73c6.81-10.74,16.72-16.73,29.11-16.73
|
||||
c20.86,0,36.75,15.07,36.75,39.23v37.99c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.57,13.22-13.01,13.22
|
||||
C437.44,685.04,431.45,679.26,431.45,671.82z"/>
|
||||
<path class="st0" d="M491.33,537.4c7.85,0,14.25,6.4,14.25,14.04c0,7.85-6.4,14.04-14.25,14.04s-14.45-6.19-14.45-14.04
|
||||
C476.87,543.8,483.48,537.4,491.33,537.4z M479.15,610.7c0-3.72-0.83-9.71-0.83-13.22c0-7.43,5.78-13.22,13.01-13.22
|
||||
s13.01,5.78,13.01,13.22c0,3.51-1.03,9.5-1.03,13.22v47.9c0,3.72,1.03,9.71,1.03,13.22c0,7.43-5.78,13.22-13.01,13.22
|
||||
s-13.01-5.78-13.01-13.22c0-3.51,0.83-9.5,0.83-13.22V610.7z"/>
|
||||
<path class="st0" d="M522.09,634.04c0-29.11,18.17-52.65,48.32-52.65c15.9,0,30.56,7.23,37.17,17.97c2.48,3.92,2.89,6.19,2.89,8.05
|
||||
c0,6.4-4.96,11.77-12.18,11.77c-4.75,0-9.08-2.68-10.94-7.43c-2.89-6.4-8.47-10.12-16.93-10.12c-15.9,0-24.78,14.25-24.78,32.21
|
||||
c0,18.17,9.29,32.21,25.4,32.21c8.67,0,14.87-3.1,17.76-9.5c2.06-4.34,5.99-8.05,11.36-8.05c7.43,0,11.98,5.16,11.98,11.56
|
||||
c0,3.1-1.24,6.81-3.92,10.32c-6.82,9.09-19.62,16.31-37.17,16.31C540.06,686.69,522.09,663.56,522.09,634.04z"/>
|
||||
<path class="st0" d="M690.17,671.82c0-3.51,0.83-9.5,0.83-13.22v-35.3c0-12.6-7.02-21.27-19-21.27c-8.26,0-15.28,3.92-19.82,10.32
|
||||
v46.25c0,3.72,0.83,9.71,0.83,13.22c0,7.43-5.78,13.22-13.01,13.22s-13.01-5.78-13.01-13.22c0-3.51,1.03-9.5,1.03-13.22v-99.94
|
||||
c0-3.72-1.03-9.71-1.03-13.22c0-7.43,5.99-13.22,13.01-13.22c7.23,0,13.01,5.78,13.01,13.22c0,3.51-0.83,9.5-0.83,13.22v33.66
|
||||
c6.2-6.81,15.07-10.94,26.43-10.94c21.27,0,36.55,15.9,36.55,38.61v38.61c0,3.72,1.03,9.71,1.03,13.22
|
||||
c0,7.43-5.99,13.22-13.01,13.22C695.95,685.04,690.17,679.26,690.17,671.82z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M376.76,216.42c28.32,25.07,51.15,51.95,65.83,77.27c25.23-45.12,42.08-98.73,42.3-132.88
|
||||
c0-0.24,0-0.46,0-0.66c0-50.53-50.41-70.2-93.82-70.2s-93.82,19.66-93.82,70.2c0,0.69,0,1.62,0,2.73
|
||||
C321.44,173.62,350.14,192.84,376.76,216.42z"/>
|
||||
<path class="st2" d="M222.27,354.21c17.7-19.69,44.85-41.04,75.5-59.08c32.6-19.19,65.21-32.59,93.83-38.73
|
||||
c-35.11-37.94-80.89-70.53-113.31-81.29c-0.23-0.07-0.44-0.14-0.63-0.21c-48.06-15.61-82.34,26.25-95.75,67.54
|
||||
c-13.42,41.29-10.29,95.31,37.77,110.92C220.33,353.58,221.21,353.86,222.27,354.21z"/>
|
||||
<path class="st3" d="M600.73,241.74c-13.42-41.29-47.69-83.15-95.75-67.54c-0.66,0.21-1.54,0.5-2.6,0.84
|
||||
c-2.75,26.34-12.16,59.57-26.36,92.17c-15.09,34.68-33.6,64.69-53.14,86.48c50.7,10.05,106.9,9.52,139.45-0.83
|
||||
c0.23-0.07,0.44-0.14,0.63-0.21C611.02,337.05,614.15,283.03,600.73,241.74z"/>
|
||||
<path class="st4" d="M348.22,394.58c-8.17-36.93-10.84-72.09-7.84-101.2c-46.93,21.67-92.08,55.14-112.33,82.64
|
||||
c-0.14,0.19-0.27,0.37-0.39,0.54c-29.7,40.88-0.48,86.42,34.64,111.94s87.46,39.24,117.16-1.64c0.41-0.56,0.95-1.31,1.6-2.21
|
||||
C367.81,461.72,355.9,429.3,348.22,394.58z"/>
|
||||
<path class="st5" d="M554.19,373.91c-25.9,5.53-60.41,6.84-95.81,3.42c-37.65-3.64-71.91-11.96-98.67-23.82
|
||||
c6.11,51.33,23.99,104.61,43.89,132.37c0.14,0.19,0.27,0.37,0.39,0.54c29.7,40.88,82.04,27.16,117.16,1.64S585.5,417,555.8,376.12
|
||||
C555.39,375.56,554.85,374.81,554.19,373.91z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -1,98 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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 |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 540 KiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 570 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 206 KiB |
@@ -99,7 +99,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 +114,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:
|
||||
|
||||
@@ -56,7 +56,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 +73,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.0-ubuntu@sha256:c1f582b7cc4c1b9805d187b5600ce7879550a12ef6d29571da133c3d3fc67a9c
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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']
|
||||
@@ -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:**
|
||||
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -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.
|
||||
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,7 +38,7 @@ Note: Either a manual or scheduled library scan must have been performed to iden
|
||||
|
||||
In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
|
||||
|
||||
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under user account settings > libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
|
||||
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
|
||||
|
||||
### Import Paths
|
||||
|
||||
@@ -50,8 +50,6 @@ If the import paths are edited in a way that an external file is no longer in an
|
||||
|
||||
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
|
||||
|
||||
- Is the external path set correctly? Each import path must be contained in the external path.
|
||||
- Make sure the external path does not contain spaces
|
||||
- In the docker-compose file, are the volumes mounted correctly?
|
||||
- Are the volumes identical between the `server` and `microservices` container?
|
||||
- Are the import paths set correctly, and do they match the path set in docker-compose file?
|
||||
@@ -61,18 +59,6 @@ Sometimes, an external library will not scan correctly. This can happen if immic
|
||||
|
||||
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
:::caution
|
||||
|
||||
Please read and understand this section before setting external paths, as there are important security considerations.
|
||||
|
||||
:::
|
||||
|
||||
For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server.
|
||||
|
||||
With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below.
|
||||
|
||||
### Exclusion Patterns
|
||||
|
||||
By default, all files in the import paths will be added to the library. If there are files that should not be added, exclusion patterns can be used to exclude them. Exclusion patterns are glob patterns are matched against the full file path. If a file matches an exclusion pattern, it will not be added to the library. Exclusion patterns can be added in the Scan Settings page for each library. Under the hood, Immich uses the [glob](https://www.npmjs.com/package/glob) package to match patterns, so please refer to [their documentation](https://github.com/isaacs/node-glob#glob-primer) to see what patterns are supported.
|
||||
@@ -90,6 +76,16 @@ This feature - currently hidden in the config file - is considered experimental
|
||||
|
||||
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
|
||||
|
||||
```
|
||||
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'
|
||||
```
|
||||
|
||||
In rare cases, the library watcher can hang, preventing Immich from starting up. In this case, disable the library watcher in the configuration file. If the watcher is enabled from within Immich, the app must be started without the microservices. Disable the microservices in the docker compose file, start Immich, disable the library watcher in the admin settings, close Immich, re-enable the microservices, and then Immich can be started normally.
|
||||
|
||||
### Nightly job
|
||||
|
||||
There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
|
||||
@@ -135,27 +131,13 @@ The `ro` flag at the end only gives read-only access to the volumes. While Immic
|
||||
_Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._
|
||||
:::
|
||||
|
||||
### Set External Path
|
||||
|
||||
Only an admin can do this.
|
||||
|
||||
- Navigate to `Administration > Users` page on the web.
|
||||
- Click on the user edit button.
|
||||
- Set `/mnt/media` to be the external path. This folder will only contain the three folders that we want to import, so nothing else can be accessed.
|
||||
:::note
|
||||
Spaces in the internal path aren't currently supported.
|
||||
|
||||
You must import it as:
|
||||
`..:/mnt/media/my-media:ro`
|
||||
instead of
|
||||
`..:/mnt/media/my media:ro`
|
||||
:::
|
||||
|
||||
### Create External Libraries
|
||||
|
||||
- Click on your user name in the top right corner -> Account Settings
|
||||
- Click on Libraries
|
||||
These actions must be performed by the Immich administrator.
|
||||
|
||||
- Click on Administration -> Libraries
|
||||
- Click on Create External Library
|
||||
- Select which user owns the library, this can not be changed later
|
||||
- Click the drop-down menu on the newly created library
|
||||
- Click on Rename Library and rename it to "Christmas Trip"
|
||||
- Click Edit Import Paths
|
||||
@@ -166,7 +148,7 @@ NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/
|
||||
|
||||
Next, we'll add an exclusion pattern to filter out raw files.
|
||||
|
||||
- Click the drop-down menu on the newly christmas library
|
||||
- Click the drop-down menu on the newly-created Christmas library
|
||||
- Click on Manage
|
||||
- Click on Scan Settings
|
||||
- Click on Add Exclusion Pattern
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Monitoring
|
||||
|
||||
## Overview
|
||||
|
||||
Immich provides a variety of performance metrics to allow for local monitoring and insights. This integration is primarily in the form of Prometheus metrics. However, exporting traces is also possible due to the use of OpenTelemetry instrumentation.
|
||||
|
||||
:::note
|
||||
This is an opt-in feature intended for you to monitor immich's performance. This data isn't sent anywhere beyond what you've configured.
|
||||
:::
|
||||
|
||||
## Prometheus
|
||||
|
||||
Prometheus is a tool that collects metrics from a number of sources you configure. It operates in a "pull" strategy - that is, it periodically requests metrics from each defined source. This means that the source doesn't send anything until it's requested. It also means that the source -- immich, in this case -- has to expose an endpoint for Prometheus to target when it requests metrics.
|
||||
|
||||
### Metrics
|
||||
|
||||
These metrics come in a variety of forms:
|
||||
|
||||
- Counters, which can only increase. Example: the number of times an endpoint has been called.
|
||||
- Gauges, which can increase or decrease within a certain range. Example: CPU utilization.
|
||||
- Histograms, where each observation is assigned to a certain number of "buckets". Example: response time, where each bucket is a number of milliseconds. This one is a bit more complicated.
|
||||
- Buckets in this case are _cumulative_; that is, an observation is placed not only into the smallest bucket that contains it, but also to all buckets larger than this. For example, if a histogram has three buckets for 1ms, 5ms and 10ms, an observation of 3ms will be bucketed into both 5ms and 10ms.
|
||||
|
||||
The metrics in immich are grouped into API (endpoint calls and response times), host (memory and CPU utilization), and IO (internal database queries, image processing, and so on). Each group of metrics can be enabled or disabled independently.
|
||||
|
||||
### Configuration
|
||||
|
||||
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable.
|
||||
|
||||
:::note
|
||||
`IMMICH_METRICS` is equivalent to enabling the following three environmental variables: `IMMICH_API_METRICS`, `IMMICH_HOST_METRICS`, and `IMMICH_IO_METRICS`. If you would like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group.
|
||||
:::
|
||||
|
||||
The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
|
||||
|
||||
You can start by defining a Prometheus service in the Compose file:
|
||||
|
||||
```yaml
|
||||
immich-prometheus:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
# this exposes the default port for Prometheus so you can interact with it
|
||||
- 9090:9090
|
||||
image: prom/prometheus
|
||||
volumes:
|
||||
# the Prometheus configuration file - a barebones one is provided to get started
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
# a named volume defined in the bottom of the Compose file; it can also be a mounted folder
|
||||
- prometheus-data:/prometheus
|
||||
```
|
||||
|
||||
You will also need to add `prometheus-data` to the list of volumes in the bottom of the Compose file:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
model-cache:
|
||||
prometheus-data:
|
||||
```
|
||||
|
||||
The last piece is the [configuration file][prom-file]. This file defines (among other things) the sources Prometheus should target. Download it and place it in the same folder as the Compose file.
|
||||
|
||||
:::tip
|
||||
The provided file is just a starting point. There are a ton of ways to configure Prometheus, so feel free to experiment!
|
||||
:::
|
||||
|
||||
After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network.
|
||||
|
||||
:::note
|
||||
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8081` to the microservices container's ports. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
||||
So after setting up Prometheus, how do you actually view the metrics? The simplest way is to use Prometheus directly. Visiting Prometheus will show you a web UI where you can search for and visualize metrics. You can also view the status of your data sources and configure settings, but this is beyond the scope of this guide.
|
||||
|
||||
## Grafana
|
||||
|
||||
For a dedicated tool with nice presentation, you can use Grafana instead. This connects to Prometheus (and possibly other sources) for sophisticated data visualization.
|
||||
|
||||
Setting up Grafana is similar to Prometheus. You can add a service for it:
|
||||
|
||||
```yaml
|
||||
immich-grafana:
|
||||
container_name: immich_grafana
|
||||
command: ['./run.sh', '-disable-reporting'] # this is to disable Grafana's telemetry
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana
|
||||
volumes:
|
||||
# stores your pretty dashboards and panels
|
||||
- grafana-data:/var/lib/grafana
|
||||
```
|
||||
|
||||
And add another volume for it:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
model-cache:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
```
|
||||
|
||||
After bringing down the services and back up again, you can now visit Grafana to view your metrics. On the first login, enter `admin` for both username and password and update your password. You can then go to the settings and add a data source with `http://immich-prometheus:9090` to point Grafana to your Prometheus instance.
|
||||
|
||||
### Usage
|
||||
|
||||
You can make your first dashboard to get started. Don't forget to save it frequently, or you'll lose all your progress!
|
||||
|
||||
You can then make a new panel, specifying Prometheus as the data source for it.
|
||||
|
||||
-- TODO: add images and more details here
|
||||
|
||||
[prom-file]: https://github.com/immich-app/immich/releases/latest/download/prometheus.yml
|
||||
@@ -0,0 +1,130 @@
|
||||
# API Album Sync (Python Script)
|
||||
|
||||
This is an example of a python script for syncing an album to a local folder. This was used for a digital photoframe so the displayed photos could be managed from the immich web or app UI.
|
||||
|
||||
The script is copied below in it's current form. A repository is hosted [here](https://git.orenit.solutions/open/immichalbumpull).
|
||||
|
||||
:::danger
|
||||
This guide uses a generated API key. This key gives the same access to your immich instance as the user it is attached to, so be careful how the config file is stored and transferred.
|
||||
:::
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.7+
|
||||
- [requests library](https://pypi.org/project/requests/)
|
||||
|
||||
### Installing
|
||||
|
||||
Copy the contents of 'pull.py' (shown below) to your chosen location or clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.orenit.solutions/open/immichalbumpull
|
||||
```
|
||||
|
||||
Edit or create the 'config.ini' file in the same directory as the script with the necessary details:
|
||||
|
||||
```ini title='config.ini'
|
||||
[immich]
|
||||
# URL of target immich instance
|
||||
url = https://photo.example.com
|
||||
# API key from Account Settings -> API Keys
|
||||
apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# Full local path to target directory
|
||||
destination = /home/photo/photos
|
||||
# immich album name
|
||||
album = Photoframe
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Run the script directly:
|
||||
|
||||
```bash
|
||||
./pull.py
|
||||
```
|
||||
|
||||
Or from cron (every 5 minutes):
|
||||
|
||||
```bash
|
||||
*/5 * * * * /usr/bin/python /home/user/immichalbumpull/pull.py
|
||||
```
|
||||
|
||||
### Python Script
|
||||
|
||||
```python title='pull.py'
|
||||
#!/usr/bin/env python
|
||||
|
||||
import requests
|
||||
import configparser
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# Read config file
|
||||
config = configparser.ConfigParser()
|
||||
config.read('config.ini')
|
||||
|
||||
url = config['immich']['url']
|
||||
apikey = config['immich']['apikey']
|
||||
photodir = config['immich']['destination']
|
||||
albumname = config['immich']['album']
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'x-api-key': apikey
|
||||
}
|
||||
|
||||
# Set up the directory for the downloaded images
|
||||
os.makedirs(photodir, exist_ok=True)
|
||||
|
||||
# Get the list of albums from the API
|
||||
response = requests.get(url + "/api/album", headers=headers)
|
||||
|
||||
# Parse the JSON response
|
||||
data = response.json()
|
||||
|
||||
# Find the chosen album id
|
||||
for item in data:
|
||||
if item['albumName'] == albumname:
|
||||
albumid = item['id']
|
||||
|
||||
# Get the list of photos from the API using the albumid
|
||||
response = requests.get(url + "/api/album/" + albumid, headers=headers)
|
||||
|
||||
# Parse the JSON response and extract the URLs of the images
|
||||
data = response.json()
|
||||
image_urls = data['assets']
|
||||
|
||||
# Download each image from the URL and save it to the directory
|
||||
headers = {
|
||||
'Accept': 'application/octet-stream',
|
||||
'x-api-key': apikey
|
||||
}
|
||||
|
||||
photolist = []
|
||||
|
||||
for id in image_urls:
|
||||
# Query asset info endpoint for correct extension
|
||||
assetinfourl = url + "/api/asset/" + str(id['id'])
|
||||
response = requests.get(assetinfourl, headers=headers)
|
||||
assetinfo = response.json()
|
||||
ext = os.path.splitext(assetinfo['originalFileName'])
|
||||
|
||||
asseturl = url + "/api/download/asset/" + str(id['id'])
|
||||
response = requests.post(asseturl, headers=headers, stream=True)
|
||||
|
||||
# Build current photo list for deletions below
|
||||
photo = os.path.basename(asseturl) + ext[1]
|
||||
photolist.append(photo)
|
||||
|
||||
photofullpath = photodir + '/' + os.path.basename(asseturl) + ext[1]
|
||||
# Only download file if it doesn't already exist
|
||||
if not os.path.exists(photofullpath):
|
||||
with open(photofullpath, 'wb') as f:
|
||||
for chunk in response.iter_content(1024):
|
||||
f.write(chunk)
|
||||
|
||||
# Delete old photos removed from album
|
||||
for filename in os.listdir(photodir):
|
||||
if filename not in photolist:
|
||||
os.unlink(os.path.join(photodir, filename))
|
||||
```
|
||||
@@ -13,7 +13,7 @@ Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to th
|
||||
## Assets
|
||||
|
||||
:::note
|
||||
The `"originalFileName"` column is the name of the uploaded file _without_ the extension.
|
||||
The `"originalFileName"` column is the name of the file at time of upload, including the extension.
|
||||
:::
|
||||
|
||||
```sql title="Find by original filename"
|
||||
@@ -40,6 +40,10 @@ SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
|
||||
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
|
||||
```
|
||||
|
||||
```sql title="size < 100,000 bytes, smallest to largest"
|
||||
SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC;
|
||||
```
|
||||
|
||||
```sql title="Without thumbnails"
|
||||
SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL;
|
||||
```
|
||||
|
||||
@@ -14,6 +14,12 @@ Edit `docker-compose.yml` to add two new mount points under `volumes:`
|
||||
- ${EXTERNAL_PATH}:/usr/src/app/external
|
||||
```
|
||||
|
||||
```
|
||||
immich-microservices:
|
||||
volumes:
|
||||
- ${EXTERNAL_PATH}:/usr/src/app/external
|
||||
```
|
||||
|
||||
Be sure to add exactly the same line to both `immich-server:` and `immich-microservices:`.
|
||||
|
||||
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
|
||||
@@ -28,6 +34,10 @@ On my computer, for example, I use this path:
|
||||
EXTERNAL_PATH=/home/tenino/photos
|
||||
```
|
||||
|
||||
:::info EXTERNAL_PATH design
|
||||
The design choice to put the EXTERNAL_PATH into .env rather than put two copies of the absolute path in the yml file in order to make everything easier, so if you have two copies of the same path that have to be kept in sync, then someday later when you move the data, update only one of the paths, without everything will break mysteriously.
|
||||
:::
|
||||
|
||||
Restart Immich.
|
||||
|
||||
```
|
||||
@@ -35,47 +45,26 @@ docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
# Set the External Path
|
||||
# Create the library
|
||||
|
||||
In the Immich web UI:
|
||||
|
||||
- click the **Administration** link in the upper right corner.
|
||||
<img src={require('./img/administration-link.png').default} width="50%" title="Administration link" />
|
||||
|
||||
- Select the **Users** tab
|
||||
<img src={require('./img/users-tab.png').default} width="50%" title="Users tab" />
|
||||
- Select the **External Libraries** tab
|
||||
<img src={require('./img/external-libraries.png').default} width="50%" title="External Libraries tab" />
|
||||
|
||||
- Select the **pencil** next to your user ID
|
||||
<img src={require('./img/pencil.png').default} width="50%" title="Pencil" />
|
||||
- Click the **Create Library** button
|
||||
<img src={require('./img/create-external-library.png').default} width="50%" title="Create Library button" />
|
||||
|
||||
- Fill in the **External Path** field with `/usr/src/app/external`
|
||||
<img src={require('./img/external-path.png').default} width="50%" title="External Path field" />
|
||||
|
||||
Notice this matches the path _inside the container_ where we mounted your photos.
|
||||
The purpose of the external path field is for administrators who have multiple users
|
||||
on their Immich instance. It lets you prevent other authorized users from
|
||||
navigating to your external library.
|
||||
|
||||
# Import the library
|
||||
|
||||
In the Immich web UI:
|
||||
|
||||
- Click your user avatar in the upper-right corner (circle with your initials)
|
||||
<img src={require('./img/user-avatar.png').default} width="50%" title="User avatar" />
|
||||
|
||||
- Click **Account Settings**
|
||||
<img src={require('./img/account-settings.png').default} width="50%" title="Account Settings button" />
|
||||
|
||||
- Click to expand **Libraries**
|
||||
<img src={require('./img/libraries-dropdown.png').default} width="50%" title="Libraries dropdown" />
|
||||
|
||||
- Click the **Create External Library** button
|
||||
<img src={require('./img/create-external-library-button.png').default} width="50%" title="Create External Library button" />
|
||||
- In the dialog, select which user should own the new library
|
||||
<img src={require('./img/library-owner.png').default} width="50%" title="Library owner diaglog" />
|
||||
|
||||
- Click the three-dots menu and select **Edit Import Paths**
|
||||
<img src={require('./img/edit-import-paths.png').default} width="50%" title="Edit Import Paths menu option" />
|
||||
|
||||
- Click \*_Add path_
|
||||
- Click Add path
|
||||
<img src={require('./img/add-path-button.png').default} width="50%" title="Add Path button" />
|
||||
|
||||
- Enter **/usr/src/app/external** as the path and click Add
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -16,7 +16,12 @@ version: '3.8'
|
||||
services:
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
||||
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||
# extends:
|
||||
# file: hwaccel.ml.yml
|
||||
# service: # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||
volumes:
|
||||
- model-cache:/cache
|
||||
restart: always
|
||||
|
||||
@@ -9,8 +9,8 @@ The database is saved to your Immich upload folder in the `database-backup` subd
|
||||
### Prerequisites
|
||||
|
||||
- Borg needs to be installed on your server as well as the remote machine. You can find instructions to install Borg [here](https://borgbackup.readthedocs.io/en/latest/installation.html).
|
||||
- To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
|
||||
- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server.
|
||||
- (Optional) To run this sript as a non-root user, you should [add your username to the docker group](https://docs.docker.com/engine/install/linux-postinstall/).
|
||||
- To run this script non-interactively, set up [passwordless ssh](https://www.redhat.com/sysadmin/passwordless-ssh) to your remote machine from your server. If you skipped the previous step, make sure this step is done from your root account.
|
||||
|
||||
To initialize the borg repository, run the following commands once.
|
||||
|
||||
@@ -19,19 +19,16 @@ UPLOAD_LOCATION="/path/to/immich/directory" # Immich database location, as
|
||||
BACKUP_PATH="/path/to/local/backup/directory"
|
||||
|
||||
mkdir "$UPLOAD_LOCATION/database-backup"
|
||||
mkdir "$BACKUP_PATH/immich-borg"
|
||||
|
||||
borg init --encryption=none "$BACKUP_PATH/immich-borg"
|
||||
|
||||
## Remote set up
|
||||
REMOTE_HOST="remote_host@IP"
|
||||
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
||||
|
||||
ssh "$REMOTE_HOST" "mkdir $REMOTE_BACKUP_PATH/immich-borg"
|
||||
ssh "$REMOTE_HOST" "borg init --encryption=none $REMOTE_BACKUP_PATH/immich-borg"
|
||||
borg init --encryption=none "$REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg"
|
||||
```
|
||||
|
||||
Edit the following script as necessary and add it to your crontab. Note that this script assumes there are no spaces in your paths. If there are spaces, enclose the paths in double quotes.
|
||||
Edit the following script as necessary and add it to your crontab. Note that this script assumes there are no `:`, `@`, or `"` characters in your paths. If these characters exist, you will need to escape and/or rename the paths.
|
||||
|
||||
```bash title='Borg backup template'
|
||||
#!/bin/sh
|
||||
@@ -46,18 +43,20 @@ REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
||||
### Local
|
||||
|
||||
# Backup Immich database
|
||||
docker exec -t immich_postgres pg_dumpall -c -U postgres | /usr/bin/gzip > $UPLOAD_LOCATION/database-backup/immich-database.sql.gz
|
||||
docker exec -t immich_postgres pg_dumpall -c -U postgres > "$UPLOAD_LOCATION"/database-backup/immich-database.sql
|
||||
# For deduplicating backup programs such as Borg or Restic, compressing the content can increase backup size by making it harder to deduplicate. If you are using a different program or still prefer to compress, you can use the following command instead:
|
||||
# docker exec -t immich_postgres pg_dumpall -c -U postgres | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/immich-database.sql.gz
|
||||
|
||||
### Append to local Borg repository
|
||||
borg create $BACKUP_PATH/immich-borg::{now} $UPLOAD_LOCATION --exclude $UPLOAD_LOCATION/thumbs/ --exclude $UPLOAD_LOCATION/encoded-video/
|
||||
borg prune --keep-weekly=4 --keep-monthly=3 $BACKUP_PATH/immich-borg
|
||||
borg compact $BACKUP_PATH/immich-borg
|
||||
borg create "$BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/
|
||||
borg prune --keep-weekly=4 --keep-monthly=3 "$BACKUP_PATH"/immich-borg
|
||||
borg compact "$BACKUP_PATH"/immich-borg
|
||||
|
||||
|
||||
### Append to remote Borg repository
|
||||
borg create $REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg::{now} $UPLOAD_LOCATION --exclude $UPLOAD_LOCATION/thumbs/ --exclude $UPLOAD_LOCATION/encoded-video/
|
||||
borg prune --keep-weekly=4 --keep-monthly=3 $REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg
|
||||
borg compact $REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg
|
||||
borg create "$REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/thumbs/ --exclude "$UPLOAD_LOCATION"/encoded-video/
|
||||
borg prune --keep-weekly=4 --keep-monthly=3 "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg
|
||||
borg compact "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg
|
||||
```
|
||||
|
||||
### Restoring
|
||||
@@ -67,7 +66,7 @@ To restore from a backup, use the `borg mount` command.
|
||||
```bash title='Restore from local backup'
|
||||
BACKUP_PATH="/path/to/local/backup/directory"
|
||||
mkdir /tmp/immich-mountpoint
|
||||
borg mount $BACKUP_PATH/immich-borg /tmp/immich-mountpoint
|
||||
borg mount "$BACKUP_PATH"/immich-borg /tmp/immich-mountpoint
|
||||
cd /tmp/immich-mountpoint
|
||||
```
|
||||
|
||||
@@ -75,7 +74,7 @@ cd /tmp/immich-mountpoint
|
||||
REMOTE_HOST="remote_host@IP"
|
||||
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
|
||||
mkdir /tmp/immich-mountpoint
|
||||
borg mount $REMOTE_HOST:$REMOTE_BACKUP_PATH/immich-borg /tmp/immich-mountpoint
|
||||
borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint
|
||||
cd /tmp/immich-mountpoint
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ A config file can be provided as an alternative to the UI configuration.
|
||||
|
||||
### Step 1 - Create a new config file
|
||||
|
||||
In JSON format, create a new config file (e.g. `immich.config`) and put it in a location that can be accessed by Immich.
|
||||
In JSON format, create a new config file (e.g. `immich.json`) and put it in a location that can be accessed by Immich.
|
||||
The default configuration looks like this:
|
||||
|
||||
```json
|
||||
@@ -14,7 +14,9 @@ The default configuration looks like this:
|
||||
"threads": 0,
|
||||
"preset": "ultrafast",
|
||||
"targetVideoCodec": "h264",
|
||||
"acceptedVideoCodecs": ["h264"],
|
||||
"targetAudioCodec": "aac",
|
||||
"acceptedAudioCodecs": ["aac", "mp3", "libopus"],
|
||||
"targetResolution": "720",
|
||||
"maxBitrate": "0",
|
||||
"bframes": -1,
|
||||
@@ -24,6 +26,7 @@ The default configuration looks like this:
|
||||
"temporalAQ": false,
|
||||
"cqMode": "auto",
|
||||
"twoPass": false,
|
||||
"preferredHwDevice": "auto",
|
||||
"transcode": "required",
|
||||
"tonemap": "hable",
|
||||
"accel": "disabled"
|
||||
@@ -38,7 +41,7 @@ The default configuration looks like this:
|
||||
"metadataExtraction": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"recognizeFaces": {
|
||||
"faceDetection": {
|
||||
"concurrency": 2
|
||||
},
|
||||
"search": {
|
||||
@@ -50,9 +53,6 @@ The default configuration looks like this:
|
||||
"library": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"storageTemplateMigration": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"migration": {
|
||||
"concurrency": 5
|
||||
},
|
||||
@@ -95,18 +95,23 @@ The default configuration looks like this:
|
||||
"issuerUrl": "",
|
||||
"clientId": "",
|
||||
"clientSecret": "",
|
||||
"mobileOverrideEnabled": false,
|
||||
"mobileRedirectUri": "",
|
||||
"scope": "openid email profile",
|
||||
"signingAlgorithm": "RS256",
|
||||
"storageLabelClaim": "preferred_username",
|
||||
"storageQuotaClaim": "immich_quota",
|
||||
"defaultStorageQuota": 0,
|
||||
"buttonText": "Login with OAuth",
|
||||
"autoRegister": true,
|
||||
"autoLaunch": false
|
||||
"autoLaunch": false,
|
||||
"mobileOverrideEnabled": false,
|
||||
"mobileRedirectUri": ""
|
||||
},
|
||||
"passwordLogin": {
|
||||
"enabled": true
|
||||
},
|
||||
"storageTemplate": {
|
||||
"enabled": false,
|
||||
"hashVerificationEnabled": true,
|
||||
"template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
|
||||
},
|
||||
"thumbnail": {
|
||||
@@ -125,6 +130,9 @@ The default configuration looks like this:
|
||||
"theme": {
|
||||
"customCss": ""
|
||||
},
|
||||
"user": {
|
||||
"deleteDelay": 7
|
||||
},
|
||||
"library": {
|
||||
"scan": {
|
||||
"enabled": true,
|
||||
@@ -135,6 +143,13 @@ The default configuration looks like this:
|
||||
"usePolling": false,
|
||||
"interval": 10000
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"externalDomain": "",
|
||||
"loginPageMessage": ""
|
||||
},
|
||||
"user": {
|
||||
"deleteDelay": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -148,3 +163,7 @@ So you can just grab it from there, paste it into a file and you're pretty much
|
||||
|
||||
In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
|
||||
For more information, refer to the [Environment Variables](/docs/install/environment-variables.md) section.
|
||||
|
||||
:::tip
|
||||
YAML-formatted config files are also supported.
|
||||
:::
|
||||
|
||||
@@ -36,7 +36,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning, web |
|
||||
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
|
||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
|
||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||
|
||||
@@ -67,7 +67,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
||||
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
||||
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
||||
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
|
||||
| `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices |
|
||||
| `DB_VECTOR_EXTENSION`<sup>\*1</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
|
||||
|
||||
\*1: This setting cannot be changed after the server has successfully started up
|
||||
@@ -124,16 +124,18 @@ Redis (Sentinel) URL example JSON before encoding:
|
||||
|
||||
## Machine Learning
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :----------------------------------------------- | :----------------------------------------------------------------- | :-----------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
|
||||
| Variable | Description | Default | Services |
|
||||
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
|
||||
|
||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ Or before beginning app installation, [create the datasets](https://www.truenas.
|
||||
Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**.
|
||||
You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on.
|
||||
|
||||
:::info Permissions
|
||||
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
|
||||
:::
|
||||
|
||||
## Installing the Immich Application
|
||||
|
||||
To install the **Immich** application, go to **Apps**, click **Discover Apps**, either begin typing Immich into the search field or scroll down to locate the **Immich** application widget.
|
||||
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 758 KiB |