Compare commits
307 Commits
v1.88.0
...
dev/metric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b422bd0f7 | ||
|
|
f33a662f48 | ||
|
|
0232655da2 | ||
|
|
ac4c57247e | ||
|
|
fb01bd956f | ||
|
|
902d4d0275 | ||
|
|
db997f9173 | ||
|
|
e9197cde67 | ||
|
|
874f707c92 | ||
|
|
8fdd3aaed1 | ||
|
|
aaa7a613b2 | ||
|
|
45cf3291a2 | ||
|
|
612590feda | ||
|
|
19ea0ead85 | ||
|
|
e47e25e671 | ||
|
|
4dd7412a86 | ||
|
|
cc2dc12f6c | ||
|
|
2790a46703 | ||
|
|
f602295bf9 | ||
|
|
092a23fd7f | ||
|
|
154292242f | ||
|
|
8295542941 | ||
|
|
4505ebc315 | ||
|
|
19d66296fe | ||
|
|
64176d2ff4 | ||
|
|
cabc2d57dd | ||
|
|
5a3a2c7293 | ||
|
|
7e216809f3 | ||
|
|
81af48af7b | ||
|
|
4e06ccd052 | ||
|
|
234449f3c6 | ||
|
|
95a7bf7fac | ||
|
|
1c69dff967 | ||
|
|
f4c5bdfa1c | ||
|
|
b40859551b | ||
|
|
4e9b96ff1a | ||
|
|
baed16dab6 | ||
|
|
a7b4727c20 | ||
|
|
9834693fab | ||
|
|
085dc6cd93 | ||
|
|
de1514a441 | ||
|
|
fade8b627f | ||
|
|
d3e1572229 | ||
|
|
ffc31f034c | ||
|
|
3beeffaaf0 | ||
|
|
b68800d45c | ||
|
|
b520955d0e | ||
|
|
6e7b3d6f24 | ||
|
|
c45e8cc170 | ||
|
|
c6f56d9591 | ||
|
|
691e20521d | ||
|
|
27f8dd6040 | ||
|
|
e3fa32ad23 | ||
|
|
08f66c2ae5 | ||
|
|
4f38a283b4 | ||
|
|
00771899da | ||
|
|
09402eb6d0 | ||
|
|
d9b5adf0f7 | ||
|
|
a15c799ba3 | ||
|
|
bda9fd9dfe | ||
|
|
19754d4b21 | ||
|
|
62347edf43 | ||
|
|
67f020380f | ||
|
|
0aae9696f6 | ||
|
|
2f95cb89c1 | ||
|
|
cb1201e690 | ||
|
|
a2deba4734 | ||
|
|
ae2608e31d | ||
|
|
d8756f3897 | ||
|
|
7839be3b49 | ||
|
|
94e11d52dc | ||
|
|
05a1283500 | ||
|
|
f8519d60c7 | ||
|
|
899c71f297 | ||
|
|
2aa5f55cbf | ||
|
|
e9a8daa924 | ||
|
|
c2cda5f3b0 | ||
|
|
e29b80845b | ||
|
|
6c3bfc6f0f | ||
|
|
9cc904fb2a | ||
|
|
3e54ee5052 | ||
|
|
458257847e | ||
|
|
16f385626e | ||
|
|
e05c7f5b76 | ||
|
|
5c8eaa6859 | ||
|
|
4c5397d7e6 | ||
|
|
502495883d | ||
|
|
2836b8cda9 | ||
|
|
e4ee224e16 | ||
|
|
d729c863c8 | ||
|
|
9768931275 | ||
|
|
f2270ad757 | ||
|
|
8e39d389b5 | ||
|
|
9bb6befc92 | ||
|
|
3a2e9b6298 | ||
|
|
a4c057ba63 | ||
|
|
679b22fada | ||
|
|
b34abf25f0 | ||
|
|
36196f2a5d | ||
|
|
f13dce7d0d | ||
|
|
e5e6fcc46d | ||
|
|
523d01068f | ||
|
|
885eba2b7c | ||
|
|
f7429c3615 | ||
|
|
ec0526dbcb | ||
|
|
7a9a9473d1 | ||
|
|
a0b2cbe123 | ||
|
|
05550647eb | ||
|
|
c7df800d27 | ||
|
|
c602eaea4a | ||
|
|
cbca69841a | ||
|
|
af7c4ae090 | ||
|
|
fba9e784fb | ||
|
|
fb4b4e5895 | ||
|
|
c8da1c07dc | ||
|
|
ed05785005 | ||
|
|
81603fddc8 | ||
|
|
ed4358741e | ||
|
|
ac2a36bd53 | ||
|
|
f6ef226b64 | ||
|
|
f798e9beed | ||
|
|
08570875eb | ||
|
|
64e985d600 | ||
|
|
e3e4fb40fd | ||
|
|
960b68b02f | ||
|
|
33529d1d9b | ||
|
|
8057c375ba | ||
|
|
d2ad01cd2f | ||
|
|
8e07b35786 | ||
|
|
b7b4483a33 | ||
|
|
3a794d7a2b | ||
|
|
68c0112aaa | ||
|
|
8847ebeef2 | ||
|
|
188cdf9367 | ||
|
|
6acd8eb4ba | ||
|
|
92b4284b5a | ||
|
|
a426ea8fbc | ||
|
|
f206cb9403 | ||
|
|
2234394aa6 | ||
|
|
8a6284569a | ||
|
|
80591ea609 | ||
|
|
2553c54b26 | ||
|
|
2f4ee622ab | ||
|
|
02d55644e5 | ||
|
|
1e99ba8167 | ||
|
|
429ad28810 | ||
|
|
7b3465621f | ||
|
|
d2fbbe790b | ||
|
|
bc65bbfcc4 | ||
|
|
e4b24b6e04 | ||
|
|
68d467f0e9 | ||
|
|
866aaf00ff | ||
|
|
e086fa6931 | ||
|
|
c174f0e871 | ||
|
|
3581069c2b | ||
|
|
c5504aae6e | ||
|
|
2e59b07cc6 | ||
|
|
c25556bb08 | ||
|
|
b9a9a3956c | ||
|
|
e1739ac4fc | ||
|
|
8736c77f7a | ||
|
|
338a028185 | ||
|
|
e2d0e944eb | ||
|
|
f53b70571b | ||
|
|
2814de4420 | ||
|
|
024fe1141b | ||
|
|
086a957a2b | ||
|
|
84c5b08c25 | ||
|
|
7e8488694d | ||
|
|
231b89c9c0 | ||
|
|
d5f6584e1d | ||
|
|
7702560b12 | ||
|
|
982183600d | ||
|
|
933c24ea6f | ||
|
|
05e9697dff | ||
|
|
259700c45f | ||
|
|
22d79850f6 | ||
|
|
56aed8246d | ||
|
|
ca1be71bca | ||
|
|
6111bf157e | ||
|
|
2195730fa6 | ||
|
|
1dc832d392 | ||
|
|
1a63d3837e | ||
|
|
bdbaa166d9 | ||
|
|
812e67d55d | ||
|
|
dfd6846deb | ||
|
|
6d3421a505 | ||
|
|
ede9de146a | ||
|
|
6959cf689b | ||
|
|
36ba48b8ae | ||
|
|
8a2b36ad55 | ||
|
|
6000c7f3bc | ||
|
|
5aa658de59 | ||
|
|
6673f1eb24 | ||
|
|
a02e91169d | ||
|
|
ec92608024 | ||
|
|
5a50d32748 | ||
|
|
cbdcbd3ab4 | ||
|
|
cb00d45e3d | ||
|
|
2b2b1bba63 | ||
|
|
031420bc78 | ||
|
|
387faa13d5 | ||
|
|
6979d43650 | ||
|
|
af8bb132d0 | ||
|
|
40964187bb | ||
|
|
fe3d951f26 | ||
|
|
6e365b37db | ||
|
|
5e55a17b2a | ||
|
|
ffecfbe075 | ||
|
|
644e52b153 | ||
|
|
b396e0eee3 | ||
|
|
8b6a79ad9e | ||
|
|
696900228b | ||
|
|
e5d083fe79 | ||
|
|
d4b3fb942f | ||
|
|
527d602a9f | ||
|
|
513f252a0c | ||
|
|
0fe704c6f9 | ||
|
|
5a2fc20b20 | ||
|
|
2a45ad147c | ||
|
|
fa3f2237eb | ||
|
|
6aa356e69f | ||
|
|
a04360f625 | ||
|
|
48c9e66ae5 | ||
|
|
05ca555b6e | ||
|
|
2bb75b6aa9 | ||
|
|
869d400617 | ||
|
|
6ae7a92e03 | ||
|
|
a67f57c0e0 | ||
|
|
b04cd4edee | ||
|
|
cd1b6e6976 | ||
|
|
1fa5e220a1 | ||
|
|
b21b7f0721 | ||
|
|
21ed8d5c79 | ||
|
|
6ac4e98d4b | ||
|
|
b0db8ed6c4 | ||
|
|
6522707b49 | ||
|
|
9483c456d4 | ||
|
|
5781ae9d82 | ||
|
|
4d727708e2 | ||
|
|
5c1c174db1 | ||
|
|
cffdd9c86a | ||
|
|
ebd64ded62 | ||
|
|
0758d55dea | ||
|
|
3992119e32 | ||
|
|
87871e4df9 | ||
|
|
ef45e9f490 | ||
|
|
4e5eef129d | ||
|
|
034b308ddc | ||
|
|
3aa2927dae | ||
|
|
c04340c63e | ||
|
|
f97dca7707 | ||
|
|
cf58649a99 | ||
|
|
e65d1d5930 | ||
|
|
ad06502539 | ||
|
|
1ffe862810 | ||
|
|
69d096df17 | ||
|
|
6d1b325b34 | ||
|
|
698226634e | ||
|
|
0108211c0f | ||
|
|
155ccbc870 | ||
|
|
f222e47651 | ||
|
|
4684094b9b | ||
|
|
8a8d3811b9 | ||
|
|
309be88ccd | ||
|
|
4987bbb712 | ||
|
|
a6676907b4 | ||
|
|
df9ec9327d | ||
|
|
030cd8c4c4 | ||
|
|
6e10d15f2c | ||
|
|
6eadca330b | ||
|
|
309ba7d67e | ||
|
|
106bae4a31 | ||
|
|
95280fd692 | ||
|
|
82fffd2c56 | ||
|
|
ff275ea175 | ||
|
|
8c2851fbc4 | ||
|
|
c1d9ce8679 | ||
|
|
29c154c681 | ||
|
|
a861b93d7d | ||
|
|
0280d15d9d | ||
|
|
c8aa782fef | ||
|
|
8ff4a08a2c | ||
|
|
af1113bf9e | ||
|
|
55f7cf3ca9 | ||
|
|
c72063280c | ||
|
|
b06c2b786c | ||
|
|
c607615e41 | ||
|
|
566471444f | ||
|
|
bf82ce24e0 | ||
|
|
afd78652f2 | ||
|
|
0f657da5a4 | ||
|
|
55fa3234fd | ||
|
|
f094ff2aa1 | ||
|
|
a13052e24c | ||
|
|
bc2c73e499 | ||
|
|
bcb885422a | ||
|
|
c438e17954 | ||
|
|
c46e82561e | ||
|
|
5c0821330f | ||
|
|
69e0db56b3 | ||
|
|
e8bf498236 | ||
|
|
9cf40afaf0 | ||
|
|
28a15365d6 | ||
|
|
d49b353c49 | ||
|
|
8b966a0f15 | ||
|
|
30e9763888 |
@@ -1,5 +1,5 @@
|
||||
.vscode/
|
||||
cli/
|
||||
|
||||
design/
|
||||
docker/
|
||||
docs/
|
||||
@@ -18,3 +18,8 @@ web/node_modules/
|
||||
web/coverage/
|
||||
web/.svelte-kit
|
||||
web/build/
|
||||
|
||||
cli/node_modules
|
||||
cli/.reverse-geocoding-dump/
|
||||
cli/upload/
|
||||
cli/dist/
|
||||
2
.gitattributes
vendored
@@ -17,3 +17,5 @@ web/src/api/open-api/**/*.md -diff -merge
|
||||
web/src/api/open-api/**/*.md linguist-generated=true
|
||||
web/src/api/open-api/**/*.ts -diff -merge
|
||||
web/src/api/open-api/**/*.ts linguist-generated=true
|
||||
|
||||
*.sh text eol=lf
|
||||
|
||||
2
.github/FUNDING.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: alextran1502
|
||||
github: immich-app
|
||||
liberapay: alex.tran1502
|
||||
custom: https://www.buymeacoffee.com/altran1502
|
||||
|
||||
29
.github/release.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: Breaking Changes 🛠
|
||||
labels:
|
||||
- breaking-change
|
||||
- title: Server
|
||||
labels:
|
||||
- 🗄️server
|
||||
- title: Mobile
|
||||
labels:
|
||||
- 📱mobile
|
||||
- title: Web
|
||||
labels:
|
||||
- 🖥️web
|
||||
- title: Machine Learning
|
||||
labels:
|
||||
- 🧠machine-learning
|
||||
- title: CLI
|
||||
labels:
|
||||
- cli
|
||||
- title: Documentation
|
||||
labels:
|
||||
- documentation
|
||||
- title: Dependency updates
|
||||
labels:
|
||||
- renovate
|
||||
- title: Other changes
|
||||
labels:
|
||||
- "*"
|
||||
6
.github/workflows/build-mobile.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Build and sign Android
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
|
||||
runs-on: macos-12
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- name: Determine ref
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ steps.get-ref.outputs.ref }}
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "12.x"
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||
|
||||
- name: Publish Android Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apk-signed
|
||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||
|
||||
3
.github/workflows/cache-cleanup.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Clean up actions cache on PR close
|
||||
name: Cache Cleanup
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
@@ -10,6 +10,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
|
||||
5
.github/workflows/cli-release.yml
vendored
@@ -1,16 +1,17 @@
|
||||
name: Publish Package to npmjs
|
||||
name: CLI Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./cli
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -73,6 +73,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/docker-cleanup.yml
vendored
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# This workflow will not trigger runs on forked repos.
|
||||
|
||||
name: Cleanup Old Docker Images
|
||||
name: Docker Cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
3
.github/workflows/docker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build and Push Docker Images
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -18,6 +18,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
# Prevent a failure in one image from stopping the other builds
|
||||
|
||||
13
.github/workflows/pr-require-label.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Enforce PR labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, edited, synchronize]
|
||||
jobs:
|
||||
enforce-label:
|
||||
name: Enforce label
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- if: toJson(github.event.pull_request.labels) == '[]'
|
||||
run: exit 1
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
token: ${{ secrets.ORG_RELEASE_TOKEN }}
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-apk-signed
|
||||
|
||||
|
||||
84
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
name: Run end-to-end test suites
|
||||
name: Server (e2e)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run e2e tests
|
||||
run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||
run: make test-server-e2e
|
||||
|
||||
doc-tests:
|
||||
name: Run documentation checks
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -45,8 +45,12 @@ jobs:
|
||||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
server-unit-tests:
|
||||
name: Run server unit test suites and checks
|
||||
name: Server
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -76,7 +80,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-unit-tests:
|
||||
name: Run cli test suites
|
||||
name: CLI
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -86,9 +90,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run npm install
|
||||
- name: Run npm install in cli
|
||||
run: npm ci
|
||||
|
||||
- name: Run npm install in server
|
||||
run: npm ci
|
||||
working-directory: ./server
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -105,8 +113,31 @@ jobs:
|
||||
run: npm run test:cov
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-e2e-tests:
|
||||
name: CLI (e2e)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./cli
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run npm install in cli
|
||||
run: npm ci
|
||||
|
||||
- name: Run npm install in server
|
||||
run: npm ci
|
||||
working-directory: ./server
|
||||
|
||||
- name: Run e2e tests
|
||||
run: npm run test:e2e
|
||||
|
||||
web-unit-tests:
|
||||
name: Run web unit test suites and checks
|
||||
name: Web
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -140,7 +171,7 @@ jobs:
|
||||
# if: ${{ !cancelled() }}
|
||||
|
||||
mobile-unit-tests:
|
||||
name: Run mobile unit tests
|
||||
name: Mobile
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -154,7 +185,7 @@ jobs:
|
||||
run: flutter test -j 1
|
||||
|
||||
ml-unit-tests:
|
||||
name: Run ML unit tests and checks
|
||||
name: Machine Learning
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -163,7 +194,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: pipx install poetry
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: "poetry"
|
||||
@@ -178,13 +209,13 @@ jobs:
|
||||
poetry run black --check app export
|
||||
- name: Run mypy type checking
|
||||
run: |
|
||||
poetry run mypy --install-types --non-interactive --strict app/ export/
|
||||
poetry run mypy --install-types --non-interactive --strict app/
|
||||
- name: Run tests and coverage
|
||||
run: |
|
||||
poetry run pytest --cov app
|
||||
|
||||
generated-api-up-to-date:
|
||||
name: Check generated files are up-to-date
|
||||
name: OpenAPI Clients
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -205,11 +236,11 @@ jobs:
|
||||
exit 1
|
||||
|
||||
generated-typeorm-migrations-up-to-date:
|
||||
name: Check generated TypeORM migrations are up-to-date
|
||||
name: TypeORM Checks
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
@@ -232,7 +263,7 @@ jobs:
|
||||
- name: Install server dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build the
|
||||
- name: Build the app
|
||||
run: npm run build
|
||||
|
||||
- name: Run existing migrations
|
||||
@@ -248,13 +279,32 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
server/src/infra/migrations/
|
||||
- name: Verify files have not changed
|
||||
- name: Verify migration files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
echo "ERROR: Generated files not up to date!"
|
||||
echo "ERROR: Generated migration files not up to date!"
|
||||
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
||||
exit 1
|
||||
|
||||
- name: Run SQL generation
|
||||
run: npm run sql:generate
|
||||
env:
|
||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@v13.1
|
||||
id: verify-changed-sql-files
|
||||
with:
|
||||
files: |
|
||||
server/src/infra/sql
|
||||
|
||||
- name: Verify SQL files have not changed
|
||||
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
echo "ERROR: Generated SQL files not up to date!"
|
||||
echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}"
|
||||
exit 1
|
||||
|
||||
# mobile-integration-tests:
|
||||
# name: Run mobile end-to-end integration tests
|
||||
# runs-on: macos-latest
|
||||
|
||||
1
.gitignore
vendored
@@ -11,3 +11,4 @@ coverage
|
||||
mobile/gradle.properties
|
||||
mobile/openapi/pubspec.lock
|
||||
mobile/*.jks
|
||||
mobile/libisar.dylib
|
||||
|
||||
9
Makefile
@@ -16,8 +16,8 @@ stage:
|
||||
pull-stage:
|
||||
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||
|
||||
test-e2e:
|
||||
docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||
test-server-e2e:
|
||||
docker compose -f ./server/test/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||
|
||||
prod:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
@@ -26,7 +26,10 @@ prod-scale:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||
|
||||
api:
|
||||
cd ./server && npm run api:generate
|
||||
npm --prefix server run api:generate
|
||||
|
||||
sql:
|
||||
npm --prefix server run sql:generate
|
||||
|
||||
attach-server:
|
||||
docker exec -it docker_immich-server_1 sh
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<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>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<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>
|
||||
|
||||
122
README_de_DE.md
Normal file
@@ -0,0 +1,122 @@
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Lizenz: MIT"></a>
|
||||
<a href="https://discord.gg/D8JsnBEuKb">
|
||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="design/immich-logo.svg" width="150" title="Login mit eigener URL">
|
||||
</p>
|
||||
<h3 align="center">Immich - Hoch performante, selbst gehostete Backup-Lösung für Fotos und Videos</h3>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
<img src="design/immich-screenshots.png" title="Haupt-Screenshot">
|
||||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<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_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
</p>
|
||||
|
||||
## Warnung
|
||||
|
||||
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung.
|
||||
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes.
|
||||
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
|
||||
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos!
|
||||
|
||||
## Inhalt
|
||||
|
||||
- [Offizielle Dokumentation](https://immich.app/docs)
|
||||
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
||||
- [Demo](#demo)
|
||||
- [Funktionen](#funktionen)
|
||||
- [Einführung](https://immich.app/docs/overview/introduction)
|
||||
- [Installation](https://immich.app/docs/install/requirements)
|
||||
- [Beitragsrichtlinien](https://immich.app/docs/overview/support-the-project)
|
||||
- [Unterstütze das Projekt](#unterstütze-das-projekt)
|
||||
|
||||
## Dokumentation
|
||||
|
||||
Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://immich.app zu finden.
|
||||
|
||||
## Demo
|
||||
|
||||
Die Web-Demo kannst Du unter https://demo.immich.app finden.
|
||||
|
||||
Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
|
||||
|
||||
```bash title="Demo Credential"
|
||||
Die Anmeldedaten
|
||||
email: demo@immich.app
|
||||
passwort: demo
|
||||
```
|
||||
|
||||
```
|
||||
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
```
|
||||
|
||||
## Funktionen
|
||||
|
||||
| Funktionen | Mobil | Web |
|
||||
| ---------------------------------------------------- | ------ | ----- |
|
||||
| Fotos & Videos hochladen und ansehen | Ja | Ja |
|
||||
| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. |
|
||||
| Selektive Auswahl von Alben zum Sichern | Ja | n. a. |
|
||||
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
|
||||
| Unterstützt mehrere Benutzer | Ja | Ja |
|
||||
| Album und geteilte Alben | Ja | Ja |
|
||||
| Scrollleiste | Ja | Ja |
|
||||
| Unterstützt RAW Formate | Ja | Ja |
|
||||
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
|
||||
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
|
||||
| Administrative Funktionen (Benutzerverwaltung) | Nein | Ja |
|
||||
| Backup im Hintergrund | Ja | n. a. |
|
||||
| Virtuelles Scrollen | Ja | Ja |
|
||||
| OAuth Unterstützung | Ja | Ja |
|
||||
| API-Schlüssel | n. a. | Ja |
|
||||
| LivePhoto/MotionPhoto Backup und Wiedergabe | Ja | Ja |
|
||||
| Benutzerdefinierte Speicherstruktur | Ja | Ja |
|
||||
| Öffentliches Teilen | Nein | Ja |
|
||||
| Archive und Favoriten | Ja | Ja |
|
||||
| Globale Karte | Ja | Ja |
|
||||
| Teilen mit Partner | Ja | Ja |
|
||||
| Gesichtserkennung und Gruppierung | Ja | Ja |
|
||||
| Rückblicke (heute vor x Jahren) | Ja | Ja |
|
||||
| Offline Unterstützung | Ja | Nein |
|
||||
| Schreibgeschützte Gallerie | Ja | Ja |
|
||||
| Gestapelte Bilder | Ja | Ja |
|
||||
|
||||
## Unterstütze das Projekt
|
||||
|
||||
Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung, um mir zusätzliche Motivation zu geben, weiterzumachen.
|
||||
|
||||
Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung.
|
||||
|
||||
Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach, das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen.
|
||||
|
||||
### Spenden
|
||||
|
||||
- [Monatliche Spende](https://github.com/sponsors/immich-app) via GitHub Sponsors
|
||||
- [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
||||
## Mitwirkende
|
||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
@@ -24,6 +24,7 @@
|
||||
<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>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<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>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<a href="README_fr_FR.md">Français</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>
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
## Declino di responsabilità
|
||||
|
||||
- ⚠️ Il progetto è in fase di sviluppo **molto avanzato**.
|
||||
- ⚠️ Il progetto è in una fase **molto intensa** di sviluppo.
|
||||
- ⚠️ Possibilità di bug e cambiamenti rilevanti.
|
||||
- ⚠️ **Non utilizzare l'app come unico salvataggio delle tue foto e dei tuoi video.**
|
||||
- ⚠️ Utilizza sempre una tecnica [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) di backup per le foto e i video a cui tieni!
|
||||
@@ -72,8 +73,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
| Funzionalità | Mobile | Web |
|
||||
| ---------------------------------------------- | ------ | --- |
|
||||
| Caricamento e visualizzazione di foto e video | Sì | Sì |
|
||||
| Backup automatico quando l'app è in esecuzione | Sì | N/A |
|
||||
| Selezione degli album per backup | Sì | N/A |
|
||||
| Backup automatico quando l'app è in esecuzione | Sì | N/D |
|
||||
| Selezione degli album per backup | Sì | N/D |
|
||||
| Download foto e video sul dispositivo | Sì | Sì |
|
||||
| Supporto multi utente | Sì | Sì |
|
||||
| Album e album condivisi | Sì | Sì |
|
||||
@@ -82,10 +83,10 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
| Visualizzazione metadata (EXIF, map) | Sì | Sì |
|
||||
| Ricerca per metadata, oggetti, volti e CLIP | Sì | Sì |
|
||||
| Funzioni di amministrazione degli utenti | No | Sì |
|
||||
| Backup in background | Sì | N/A |
|
||||
| Backup in background | Sì | N/D |
|
||||
| Scroll virtuale | Sì | Sì |
|
||||
| Supporto OAuth | Sì | Sì |
|
||||
| API Keys | N/A | Sì |
|
||||
| API Keys | N/D | Sì |
|
||||
| Backup e riproduzione di LivePhoto | iOS | Sì |
|
||||
| Archiviazione impostata dall'utente | Sì | Sì |
|
||||
| Condivisione pubblica | No | Sì |
|
||||
@@ -96,6 +97,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
| Ricordi (x anni fa) | Sì | Sì |
|
||||
| Supporto offline | Sì | No |
|
||||
| Galleria sola lettura | Sì | Sì |
|
||||
| Foto raggruppate | Sì | Sì |
|
||||
|
||||
# Supporta il progetto
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</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>
|
||||
@@ -101,7 +102,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
|
||||
私はこのプロジェクトにコミットしてきました。ドキュメントを更新し、新しい機能を追加し、バグを修正し続けるつもりですが、私ひとりではできません。だから、続けるためのモチベーションをさらに高めてくれる皆さんの助けが必要なのです。
|
||||
|
||||
[selfhosted.show - In the episode 'The-organization-must-いいえt-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) のホストが言ったように、これはチームと私がやっていることの大規模な事業だ。そしていつの日か、フルタイムでこの仕事ができるようになりたいと思っています。
|
||||
[selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) のホストが言ったように、これはチームと私がやっていることの大規模な事業だ。そしていつの日か、フルタイムでこの仕事ができるようになりたいと思っています。
|
||||
|
||||
もし、あなたがこのプロジェクトに賛同し、このアプリを長く使い続けたいと思われるのであれば、以下のオプションから支援をご検討ください。
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<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_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>
|
||||
@@ -113,4 +114,4 @@ password: demo
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<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_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
</p>
|
||||
@@ -101,7 +102,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
|
||||
Ik ben trouw aan dit project en ik zal niet stoppen. Ik zal de documenten blijven bijwerken, nieuwe functies toevoegen en bugs oplossen. Maar ik kan het niet alleen. Ik heb dus jouw hulp nodig om mij extra motivatie te geven om door te gaan.
|
||||
|
||||
Als onze gastheren in de [selfhosted.show - In de aflevering 'The-organization-must-Neet-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) zeiden, dit is een eNeerme onderneming van wat het team en ik doen. En ik zou dit graag fulltime willen doen, ik vraag jouw hulp om dat mogelijk te maken.
|
||||
Als onze gastheren in de [selfhosted.show - In de aflevering 'The-organization-must-Neet-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) zeiden, dit is een enorme onderneming van wat het team en ik doen. En ik zou dit graag fulltime willen doen, ik vraag jouw hulp om dat mogelijk te maken.
|
||||
|
||||
Als je denkt dat dit het juiste doel is en de app iets is dat je jezelf al heel lang ziet gebruiken, overweeg dan om het project te steunen met de onderstaande optie.
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<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_zh_CN.md">中文</a>
|
||||
</p>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<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>
|
||||
</p>
|
||||
|
||||
4
cli/.gitignore
vendored
@@ -10,4 +10,6 @@ oclif.manifest.json
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
/coverage/
|
||||
/coverage/
|
||||
.reverse-geocoding-dump/
|
||||
upload/
|
||||
@@ -1,4 +1,6 @@
|
||||
**/*.spec.js
|
||||
test/**
|
||||
upload/**
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
|
||||
19
cli/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:20231109 as test
|
||||
|
||||
WORKDIR /usr/src/app/server
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY ./server/ .
|
||||
|
||||
WORKDIR /usr/src/app/cli
|
||||
COPY cli/package.json cli/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY ./cli/ .
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:20231109
|
||||
|
||||
VOLUME /usr/src/app/upload
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
3157
cli/package-lock.json
generated
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.5",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"immich": "./dist/index.js"
|
||||
"immich": "./dist/src/index.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"immich",
|
||||
"cli"
|
||||
],
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"byte-size": "^8.1.1",
|
||||
@@ -14,9 +18,11 @@
|
||||
"commander": "^11.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testcontainers/postgresql": "^10.4.0",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
@@ -25,14 +31,15 @@
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^49.0.0",
|
||||
"immich": "file:../server",
|
||||
"jest": "^29.5.0",
|
||||
"jest-extended": "^4.0.0",
|
||||
"jest-message-util": "^29.5.0",
|
||||
@@ -42,17 +49,19 @@
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prepack": "npm run build",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"check": "tsc --noEmit"
|
||||
"check": "tsc --noEmit",
|
||||
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand"
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
@@ -67,17 +76,22 @@
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.(t|j)s"
|
||||
"<rootDir>/src/**/*.(t|j)s",
|
||||
"!**/open-api/**"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@api(|/.*)$": "<rootDir>/src/api/$1"
|
||||
"^@api(|/.*)$": "<rootDir>/src/api/$1",
|
||||
"^@test(|/.*)$": "<rootDir>../server/test/$1",
|
||||
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
|
||||
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
|
||||
"^@app/domain(|/.*)$": "<rootDir>../server/src/domain/$1"
|
||||
},
|
||||
"coverageDirectory": "./coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/immich-app/immich.git",
|
||||
"url": "github:immich-app/immich",
|
||||
"directory": "cli"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UserApi,
|
||||
} from './open-api';
|
||||
import { ApiConfiguration } from '../cores/api-configuration';
|
||||
import FormData from 'form-data';
|
||||
|
||||
export class ImmichApi {
|
||||
public userApi: UserApi;
|
||||
@@ -35,6 +36,7 @@ export class ImmichApi {
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
},
|
||||
formDataCtor: FormData,
|
||||
});
|
||||
|
||||
this.userApi = new UserApi(this.config);
|
||||
|
||||
1157
cli/src/api/open-api/api.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.88.0
|
||||
* The version of the OpenAPI document: 1.91.4
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.88.0
|
||||
* The version of the OpenAPI document: 1.91.4
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.88.0
|
||||
* The version of the OpenAPI document: 1.91.4
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.88.0
|
||||
* The version of the OpenAPI document: 1.91.4
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ImmichApi } from '../api/client';
|
||||
import path from 'node:path';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { LoginError } from '../cores/errors/login-error';
|
||||
import { exit } from 'node:process';
|
||||
import os from 'os';
|
||||
import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
|
||||
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
|
||||
|
||||
export abstract class BaseCommand {
|
||||
protected sessionService!: SessionService;
|
||||
@@ -12,14 +11,11 @@ export abstract class BaseCommand {
|
||||
protected user!: UserResponseDto;
|
||||
protected serverVersion!: ServerVersionResponseDto;
|
||||
|
||||
protected configDir;
|
||||
protected authPath;
|
||||
|
||||
constructor() {
|
||||
const userHomeDir = os.homedir();
|
||||
this.configDir = path.join(userHomeDir, '.config/immich/');
|
||||
this.sessionService = new SessionService(this.configDir);
|
||||
this.authPath = path.join(this.configDir, 'auth.yml');
|
||||
constructor(options: BaseOptionsDto) {
|
||||
if (!options.config) {
|
||||
throw new Error('Config directory is required');
|
||||
}
|
||||
this.sessionService = new SessionService(options.config);
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
|
||||
@@ -2,10 +2,12 @@ import { Asset } from '../cores/models/asset';
|
||||
import { CrawlService } from '../services';
|
||||
import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
|
||||
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import cliProgress from 'cli-progress';
|
||||
import byteSize from 'byte-size';
|
||||
import { BaseCommand } from '../cli/base-command';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import FormData from 'form-data';
|
||||
|
||||
export default class Upload extends BaseCommand {
|
||||
uploadLength!: number;
|
||||
@@ -13,8 +15,6 @@ export default class Upload extends BaseCommand {
|
||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||
await this.connect();
|
||||
|
||||
const deviceId = 'CLI';
|
||||
|
||||
const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video);
|
||||
|
||||
@@ -22,15 +22,28 @@ export default class Upload extends BaseCommand {
|
||||
crawlOptions.pathsToCrawl = paths;
|
||||
crawlOptions.recursive = options.recursive;
|
||||
crawlOptions.exclusionPatterns = options.exclusionPatterns;
|
||||
crawlOptions.includeHidden = options.includeHidden;
|
||||
|
||||
const files: string[] = [];
|
||||
|
||||
for (const pathArgument of paths) {
|
||||
const fileStat = await fs.promises.lstat(pathArgument);
|
||||
|
||||
if (fileStat.isFile()) {
|
||||
files.push(pathArgument);
|
||||
}
|
||||
}
|
||||
|
||||
const crawledFiles: string[] = await crawlService.crawl(crawlOptions);
|
||||
|
||||
crawledFiles.push(...files);
|
||||
|
||||
if (crawledFiles.length === 0) {
|
||||
console.log('No assets found, exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const assetsToUpload = crawledFiles.map((path) => new Asset(path, deviceId));
|
||||
const assetsToUpload = crawledFiles.map((path) => new Asset(path));
|
||||
|
||||
const uploadProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
@@ -49,6 +62,10 @@ export default class Upload extends BaseCommand {
|
||||
// Compute total size first
|
||||
await asset.process();
|
||||
totalSize += asset.fileSize;
|
||||
|
||||
if (options.albumName) {
|
||||
asset.albumName = options.albumName;
|
||||
}
|
||||
}
|
||||
|
||||
const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data;
|
||||
@@ -63,6 +80,10 @@ export default class Upload extends BaseCommand {
|
||||
});
|
||||
|
||||
let skipUpload = false;
|
||||
|
||||
let skipAsset = false;
|
||||
let existingAssetId: string | undefined = undefined;
|
||||
|
||||
if (!options.skipHash) {
|
||||
const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
|
||||
|
||||
@@ -71,13 +92,24 @@ export default class Upload extends BaseCommand {
|
||||
});
|
||||
|
||||
skipUpload = checkResponse.data.results[0].action === 'reject';
|
||||
|
||||
const isDuplicate = checkResponse.data.results[0].reason === 'duplicate';
|
||||
if (isDuplicate) {
|
||||
existingAssetId = checkResponse.data.results[0].assetId;
|
||||
}
|
||||
|
||||
skipAsset = skipUpload && !isDuplicate;
|
||||
}
|
||||
|
||||
if (!skipUpload) {
|
||||
if (!skipAsset) {
|
||||
if (!options.dryRun) {
|
||||
const res = await this.immichApi.assetApi.uploadFile(asset.getUploadFileRequest());
|
||||
if (!skipUpload) {
|
||||
const formData = asset.getUploadFormData();
|
||||
const res = await this.uploadAsset(formData);
|
||||
existingAssetId = res.data.id;
|
||||
}
|
||||
|
||||
if (options.album && asset.albumName) {
|
||||
if ((options.album || options.albumName) && asset.albumName !== undefined) {
|
||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
||||
if (!album) {
|
||||
const res = await this.immichApi.albumApi.createAlbum({
|
||||
@@ -87,7 +119,12 @@ export default class Upload extends BaseCommand {
|
||||
existingAlbums.push(album);
|
||||
}
|
||||
|
||||
await this.immichApi.albumApi.addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: [res.data.id] } });
|
||||
if (existingAssetId) {
|
||||
await this.immichApi.albumApi.addAssetsToAlbum({
|
||||
id: album.id,
|
||||
bulkIdsDto: { ids: [existingAssetId] },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,4 +171,24 @@ export default class Upload extends BaseCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadAsset(data: FormData): Promise<axios.AxiosResponse> {
|
||||
const url = this.immichApi.apiConfiguration.instanceUrl + '/asset/upload';
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method: 'post',
|
||||
maxRedirects: 0,
|
||||
url,
|
||||
headers: {
|
||||
'x-api-key': this.immichApi.apiConfiguration.apiKey,
|
||||
...data.getHeaders(),
|
||||
},
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
data,
|
||||
};
|
||||
|
||||
const res = await axios(config);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
37
cli/src/constants.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import pkg from '../package.json';
|
||||
|
||||
export interface ICLIVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
}
|
||||
|
||||
export class CLIVersion implements ICLIVersion {
|
||||
constructor(
|
||||
public readonly major: number,
|
||||
public readonly minor: number,
|
||||
public readonly patch: number,
|
||||
) {}
|
||||
|
||||
toString() {
|
||||
return `${this.major}.${this.minor}.${this.patch}`;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const { major, minor, patch } = this;
|
||||
return { major, minor, patch };
|
||||
}
|
||||
|
||||
static fromString(version: string): CLIVersion {
|
||||
const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
|
||||
const matchResult = version.match(regex);
|
||||
if (matchResult) {
|
||||
const [, major, minor, patch] = matchResult.map(Number);
|
||||
return new CLIVersion(major, minor, patch);
|
||||
} else {
|
||||
throw new Error(`Invalid version format: ${version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cliVersion = CLIVersion.fromString(pkg.version);
|
||||
3
cli/src/cores/dto/base-options-dto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class BaseOptionsDto {
|
||||
config?: string;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
export class UploadOptionsDto {
|
||||
recursive = false;
|
||||
exclusionPatterns!: string[];
|
||||
dryRun = false;
|
||||
skipHash = false;
|
||||
delete = false;
|
||||
readOnly = true;
|
||||
album = false;
|
||||
recursive? = false;
|
||||
exclusionPatterns?: string[] = [];
|
||||
dryRun? = false;
|
||||
skipHash? = false;
|
||||
delete? = false;
|
||||
album? = false;
|
||||
albumName? = '';
|
||||
includeHidden? = false;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ export class LoginError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
// assign the error class name in your custom error (as a shortcut)
|
||||
this.name = this.constructor.name;
|
||||
|
||||
// capturing the stack trace keeps the reference to your error class
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'graceful-fs';
|
||||
import { basename } from 'node:path';
|
||||
import crypto from 'crypto';
|
||||
import { AssetApiUploadFileRequest } from 'src/api/open-api';
|
||||
import Os from 'os';
|
||||
import FormData from 'form-data';
|
||||
|
||||
export class Asset {
|
||||
readonly path: string;
|
||||
readonly deviceId!: string;
|
||||
|
||||
assetData?: File;
|
||||
assetData?: fs.ReadStream;
|
||||
deviceAssetId?: string;
|
||||
fileCreatedAt?: string;
|
||||
fileModifiedAt?: string;
|
||||
sidecarData?: File;
|
||||
sidecarData?: fs.ReadStream;
|
||||
sidecarPath?: string;
|
||||
fileSize!: number;
|
||||
albumName?: string;
|
||||
|
||||
constructor(path: string, deviceId: string) {
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
async process() {
|
||||
@@ -30,37 +29,45 @@ export class Asset {
|
||||
this.fileSize = stats.size;
|
||||
this.albumName = this.extractAlbumName();
|
||||
|
||||
this.assetData = await this.getFileObject(this.path);
|
||||
this.assetData = this.getReadStream(this.path);
|
||||
|
||||
// TODO: doesn't xmp replace the file extension? Will need investigation
|
||||
const sideCarPath = `${this.path}.xmp`;
|
||||
try {
|
||||
fs.accessSync(sideCarPath, fs.constants.R_OK);
|
||||
this.sidecarData = await this.getFileObject(sideCarPath);
|
||||
this.sidecarData = this.getReadStream(sideCarPath);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
getUploadFileRequest(): AssetApiUploadFileRequest {
|
||||
getUploadFormData(): FormData {
|
||||
if (!this.assetData) throw new Error('Asset data not set');
|
||||
if (!this.deviceAssetId) throw new Error('Device asset id not set');
|
||||
if (!this.fileCreatedAt) throw new Error('File created at not set');
|
||||
if (!this.fileModifiedAt) throw new Error('File modified at not set');
|
||||
if (!this.deviceId) throw new Error('Device id not set');
|
||||
|
||||
return {
|
||||
assetData: this.assetData,
|
||||
const data: any = {
|
||||
assetData: this.assetData as any,
|
||||
deviceAssetId: this.deviceAssetId,
|
||||
deviceId: this.deviceId,
|
||||
deviceId: 'CLI',
|
||||
fileCreatedAt: this.fileCreatedAt,
|
||||
fileModifiedAt: this.fileModifiedAt,
|
||||
isFavorite: false,
|
||||
sidecarData: this.sidecarData,
|
||||
isFavorite: String(false),
|
||||
};
|
||||
const formData = new FormData();
|
||||
|
||||
for (const prop in data) {
|
||||
formData.append(prop, data[prop]);
|
||||
}
|
||||
|
||||
if (this.sidecarData) {
|
||||
formData.append('sidecarData', this.sidecarData);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
private async getFileObject(path: string): Promise<File> {
|
||||
const buffer = await fs.promises.readFile(path);
|
||||
return new File([buffer], basename(path));
|
||||
private getReadStream(path: string): fs.ReadStream {
|
||||
return fs.createReadStream(path);
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
import { program, Option } from 'commander';
|
||||
import { Option, Command } from 'commander';
|
||||
import Upload from './commands/upload';
|
||||
import ServerInfo from './commands/server-info';
|
||||
import LoginKey from './commands/login/key';
|
||||
import Logout from './commands/logout';
|
||||
import { version } from '../package.json';
|
||||
|
||||
program.name('immich').description('Immich command line interface');
|
||||
import path from 'node:path';
|
||||
import os from 'os';
|
||||
|
||||
const userHomeDir = os.homedir();
|
||||
const configDir = path.join(userHomeDir, '.config/immich/');
|
||||
|
||||
const program = new Command()
|
||||
.name('immich')
|
||||
.version(version)
|
||||
.description('Command line interface for Immich')
|
||||
.addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDir));
|
||||
|
||||
program
|
||||
.command('upload')
|
||||
@@ -15,11 +26,17 @@ program
|
||||
.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('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
|
||||
.addOption(new Option('-i, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false))
|
||||
.addOption(
|
||||
new Option('-a, --album', 'Automatically create albums based on folder name')
|
||||
.env('IMMICH_AUTO_CREATE_ALBUM')
|
||||
.default(false),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-A, --album-name <name>', 'Add all assets to specified album')
|
||||
.env('IMMICH_ALBUM_NAME')
|
||||
.conflicts('album'),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
|
||||
.env('IMMICH_DRY_RUN')
|
||||
@@ -29,14 +46,14 @@ program
|
||||
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||
.action(async (paths, options) => {
|
||||
options.exclusionPatterns = options.ignore;
|
||||
await new Upload().run(paths, options);
|
||||
await new Upload(program.opts()).run(paths, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('server-info')
|
||||
.description('Display server information')
|
||||
.action(async () => {
|
||||
await new ServerInfo().run();
|
||||
await new ServerInfo(program.opts()).run();
|
||||
});
|
||||
|
||||
program
|
||||
@@ -45,14 +62,14 @@ program
|
||||
.argument('[instanceUrl]')
|
||||
.argument('[apiKey]')
|
||||
.action(async (paths, options) => {
|
||||
await new LoginKey().run(paths, options);
|
||||
await new LoginKey(program.opts()).run(paths, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('logout')
|
||||
.description('Remove stored credentials')
|
||||
.action(async () => {
|
||||
await new Logout().run();
|
||||
await new Logout(program.opts()).run();
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
@@ -19,7 +19,7 @@ const tests: Test[] = [
|
||||
files: {},
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single path',
|
||||
test: 'should crawl a single folder',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
},
|
||||
@@ -27,6 +27,25 @@ const tests: Test[] = [
|
||||
'/photos/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single file',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/image.jpg'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single file and a folder',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/image.jpg', '/images/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/images/image2.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should exclude by file extension',
|
||||
options: {
|
||||
@@ -54,6 +73,7 @@ const tests: Test[] = [
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
exclusionPatterns: ['**/raw/**'],
|
||||
recursive: true,
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
@@ -98,6 +118,7 @@ const tests: Test[] = [
|
||||
test: 'should crawl a single path',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
recursive: true,
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
|
||||
import { glob } from 'glob';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export class CrawlService {
|
||||
private readonly extensions!: string[];
|
||||
@@ -8,21 +9,57 @@ export class CrawlService {
|
||||
this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
|
||||
}
|
||||
|
||||
crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
|
||||
async crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
|
||||
const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions;
|
||||
if (!pathsToCrawl) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const base = pathsToCrawl.length === 1 ? pathsToCrawl[0] : `{${pathsToCrawl.join(',')}}`;
|
||||
const extensions = `*{${this.extensions}}`;
|
||||
const patterns: string[] = [];
|
||||
const crawledFiles: string[] = [];
|
||||
|
||||
return glob(`${base}/**/${extensions}`, {
|
||||
for await (const currentPath of pathsToCrawl) {
|
||||
try {
|
||||
const stats = await fs.promises.stat(currentPath);
|
||||
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||
crawledFiles.push(currentPath);
|
||||
} else {
|
||||
patterns.push(currentPath);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
patterns.push(currentPath);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let searchPattern: string;
|
||||
if (patterns.length === 1) {
|
||||
searchPattern = patterns[0];
|
||||
} else if (patterns.length === 0) {
|
||||
return crawledFiles;
|
||||
} else {
|
||||
searchPattern = '{' + patterns.join(',') + '}';
|
||||
}
|
||||
|
||||
if (crawlOptions.recursive) {
|
||||
searchPattern = searchPattern + '/**/';
|
||||
}
|
||||
|
||||
searchPattern = `${searchPattern}/*.{${this.extensions.join(',')}}`;
|
||||
|
||||
const globbedFiles = await glob(searchPattern, {
|
||||
absolute: true,
|
||||
nocase: true,
|
||||
nodir: true,
|
||||
dot: includeHidden,
|
||||
ignore: exclusionPatterns,
|
||||
});
|
||||
|
||||
const returnedFiles = crawledFiles.concat(globbedFiles);
|
||||
returnedFiles.sort();
|
||||
return returnedFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { SessionService } from './session.service';
|
||||
import mockfs from 'mock-fs';
|
||||
import fs from 'node:fs';
|
||||
import yaml from 'yaml';
|
||||
import { LoginError } from '../cores/errors/login-error';
|
||||
import {
|
||||
TEST_AUTH_FILE,
|
||||
TEST_CONFIG_DIR,
|
||||
TEST_IMMICH_API_KEY,
|
||||
TEST_IMMICH_INSTANCE_URL,
|
||||
createTestAuthFile,
|
||||
deleteAuthFile,
|
||||
readTestAuthFile,
|
||||
spyOnConsole,
|
||||
} from '../../test/cli-test-utils';
|
||||
|
||||
const mockPingServer = jest.fn(() => Promise.resolve({ data: { res: 'pong' } }));
|
||||
const mockUserInfo = jest.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } }));
|
||||
@@ -22,74 +31,85 @@ jest.mock('../api/open-api', () => {
|
||||
|
||||
describe('SessionService', () => {
|
||||
let sessionService: SessionService;
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
// Write a dummy output before mock-fs to prevent some annoying errors
|
||||
console.log();
|
||||
consoleSpy = spyOnConsole();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const configDir = '/config';
|
||||
sessionService = new SessionService(configDir);
|
||||
deleteAuthFile();
|
||||
sessionService = new SessionService(TEST_CONFIG_DIR);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deleteAuthFile();
|
||||
});
|
||||
|
||||
it('should connect to immich', async () => {
|
||||
mockfs({
|
||||
'/config/auth.yml': 'apiKey: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\ninstanceUrl: https://test/api',
|
||||
});
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
apiKey: TEST_IMMICH_API_KEY,
|
||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
||||
}),
|
||||
);
|
||||
|
||||
await sessionService.connect();
|
||||
expect(mockPingServer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should error if no auth file exists', async () => {
|
||||
mockfs();
|
||||
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 () => {
|
||||
mockfs({
|
||||
'/config/auth.yml': 'foo: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\napiKey: https://test/api',
|
||||
});
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
apiKey: TEST_IMMICH_API_KEY,
|
||||
}),
|
||||
);
|
||||
await sessionService.connect().catch((error) => {
|
||||
expect(error).toBeInstanceOf(LoginError);
|
||||
expect(error.message).toEqual('Instance URL missing in auth config file /config/auth.yml');
|
||||
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 () => {
|
||||
mockfs({
|
||||
'/config/auth.yml': 'instanceUrl: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\nbar: https://test/api',
|
||||
});
|
||||
await sessionService.connect().catch((error) => {
|
||||
expect(error).toBeInstanceOf(LoginError);
|
||||
expect(error.message).toEqual('API key missing in auth config file /config/auth.yml');
|
||||
});
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(sessionService.connect()).rejects.toThrow(
|
||||
new LoginError(`API key missing in auth config file ${TEST_AUTH_FILE}`),
|
||||
);
|
||||
});
|
||||
|
||||
it.skip('should create auth file when logged in', async () => {
|
||||
mockfs();
|
||||
it('should create auth file when logged in', async () => {
|
||||
await sessionService.keyLogin(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY);
|
||||
|
||||
await sessionService.keyLogin('https://test/api', 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg');
|
||||
|
||||
const data: string = await fs.promises.readFile('/config/auth.yml', 'utf8');
|
||||
const data: string = await readTestAuthFile();
|
||||
const authConfig = yaml.parse(data);
|
||||
expect(authConfig.instanceUrl).toBe('https://test/api');
|
||||
expect(authConfig.apiKey).toBe('pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg');
|
||||
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 () => {
|
||||
mockfs({
|
||||
'/config/auth.yml': 'apiKey: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\ninstanceUrl: https://test/api',
|
||||
});
|
||||
await createTestAuthFile(
|
||||
JSON.stringify({
|
||||
apiKey: TEST_IMMICH_API_KEY,
|
||||
instanceUrl: TEST_IMMICH_INSTANCE_URL,
|
||||
}),
|
||||
);
|
||||
await sessionService.logout();
|
||||
|
||||
await fs.promises.access('/auth.yml', fs.constants.F_OK).catch((error) => {
|
||||
await fs.promises.access(TEST_AUTH_FILE, fs.constants.F_OK).catch((error) => {
|
||||
expect(error.message).toContain('ENOENT');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockfs.restore();
|
||||
expect(consoleSpy.mock.calls).toEqual([[`Removed auth file ${TEST_AUTH_FILE}`]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,33 +5,39 @@ import { ImmichApi } from '../api/client';
|
||||
import { LoginError } from '../cores/errors/login-error';
|
||||
|
||||
export class SessionService {
|
||||
readonly configDir: string;
|
||||
readonly configDir!: string;
|
||||
readonly authPath!: string;
|
||||
private api!: ImmichApi;
|
||||
|
||||
constructor(configDir: string) {
|
||||
this.configDir = configDir;
|
||||
this.authPath = path.join(this.configDir, 'auth.yml');
|
||||
this.authPath = path.join(configDir, '/auth.yml');
|
||||
}
|
||||
|
||||
public async connect(): Promise<ImmichApi> {
|
||||
await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new LoginError('No auth file exist. Please login first');
|
||||
let instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
||||
let apiKey = process.env.IMMICH_API_KEY;
|
||||
|
||||
if (!instanceUrl || !apiKey) {
|
||||
await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new LoginError('No auth file exist. Please login first');
|
||||
}
|
||||
});
|
||||
|
||||
const data: string = await fs.promises.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}`);
|
||||
}
|
||||
});
|
||||
|
||||
const data: string = await fs.promises.readFile(this.authPath, 'utf8');
|
||||
const parsedConfig = yaml.parse(data);
|
||||
const instanceUrl: string = parsedConfig.instanceUrl;
|
||||
const apiKey: string = 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);
|
||||
if (!apiKey) {
|
||||
throw new LoginError(`API key missing in auth config file ${this.authPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.api = new ImmichApi(instanceUrl, apiKey);
|
||||
@@ -59,10 +65,6 @@ export class SessionService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.configDir)) {
|
||||
console.error('waah');
|
||||
}
|
||||
|
||||
fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey }));
|
||||
|
||||
console.log('Wrote auth info to ' + this.authPath);
|
||||
@@ -82,7 +84,7 @@ export class SessionService {
|
||||
});
|
||||
|
||||
if (pingResponse.res !== 'pong') {
|
||||
throw new Error('Unexpected ping reply');
|
||||
throw new Error(`Could not parse response. Is Immich listening on ${this.api.apiConfiguration.instanceUrl}?`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
cli/test/cli-test-utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export const TEST_CONFIG_DIR = '/tmp/immich/';
|
||||
export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
|
||||
export const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
|
||||
export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
|
||||
|
||||
export const CLI_BASE_OPTIONS: BaseOptionsDto = { config: TEST_CONFIG_DIR };
|
||||
|
||||
export const spyOnConsole = () => jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
export const createTestAuthFile = async (contents: string) => {
|
||||
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
||||
// Create config folder if it doesn't exist
|
||||
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(TEST_AUTH_FILE, contents);
|
||||
};
|
||||
|
||||
export const readTestAuthFile = async (): Promise<string> => {
|
||||
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
|
||||
};
|
||||
|
||||
export const deleteAuthFile = () => {
|
||||
try {
|
||||
fs.unlinkSync(TEST_AUTH_FILE);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
24
cli/test/e2e/jest-e2e.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"modulePaths": ["<rootDir>"],
|
||||
"rootDir": "../..",
|
||||
"globalSetup": "<rootDir>/test/e2e/setup.ts",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"testTimeout": 6000000,
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.(t|j)s",
|
||||
"!<rootDir>/src/**/*.spec.(t|s)s",
|
||||
"!<rootDir>/src/infra/migrations/**"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"moduleNameMapper": {
|
||||
"^@test(|/.*)$": "<rootDir>../server/test/$1",
|
||||
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
|
||||
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
|
||||
"^@app/domain(|/.*)$": "<rootDir>/../server/src/domain/$1"
|
||||
}
|
||||
}
|
||||
48
cli/test/e2e/login-key.e2e-spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { api } from '@test/api';
|
||||
import { restoreTempFolder, testApp } from 'immich/test/test-utils';
|
||||
import { LoginResponseDto } from 'src/api/open-api';
|
||||
import { APIKeyCreateResponseDto } from '@app/domain';
|
||||
import LoginKey from 'src/commands/login/key';
|
||||
import { LoginError } from 'src/cores/errors/login-error';
|
||||
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
|
||||
|
||||
describe(`login-key (e2e)`, () => {
|
||||
let server: any;
|
||||
let admin: LoginResponseDto;
|
||||
let apiKey: APIKeyCreateResponseDto;
|
||||
let instanceUrl: string;
|
||||
spyOnConsole();
|
||||
|
||||
beforeAll(async () => {
|
||||
server = (await testApp.create()).getHttpServer();
|
||||
if (!process.env.IMMICH_INSTANCE_URL) {
|
||||
throw new Error('IMMICH_INSTANCE_URL environment variable not set');
|
||||
} else {
|
||||
instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
|
||||
process.env.IMMICH_API_KEY = apiKey.secret;
|
||||
});
|
||||
|
||||
it('should error when providing an invalid API key', async () => {
|
||||
await expect(async () => await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow(
|
||||
new LoginError(`Failed to connect to server ${instanceUrl}: Request failed with status code 401`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log in when providing the correct API key', async () => {
|
||||
await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, apiKey.secret);
|
||||
});
|
||||
});
|
||||
42
cli/test/e2e/server-info.e2e-spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { api } from '@test/api';
|
||||
import { restoreTempFolder, testApp } from 'immich/test/test-utils';
|
||||
import { LoginResponseDto } from 'src/api/open-api';
|
||||
import ServerInfo from 'src/commands/server-info';
|
||||
import { APIKeyCreateResponseDto } from '@app/domain';
|
||||
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
|
||||
|
||||
describe(`server-info (e2e)`, () => {
|
||||
let server: any;
|
||||
let admin: LoginResponseDto;
|
||||
let apiKey: APIKeyCreateResponseDto;
|
||||
const consoleSpy = spyOnConsole();
|
||||
|
||||
beforeAll(async () => {
|
||||
server = (await testApp.create()).getHttpServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
|
||||
process.env.IMMICH_API_KEY = apiKey.secret;
|
||||
});
|
||||
|
||||
it('should show server version', async () => {
|
||||
await new ServerInfo(CLI_BASE_OPTIONS).run();
|
||||
|
||||
expect(consoleSpy.mock.calls).toEqual([
|
||||
[expect.stringMatching(new RegExp('Server is running version \\d+.\\d+.\\d+'))],
|
||||
[expect.stringMatching('Supported image types: .*')],
|
||||
[expect.stringMatching('Supported video types: .*')],
|
||||
['Images: 0, Videos: 0, Total: 0'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
43
cli/test/e2e/setup.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import path from 'path';
|
||||
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
||||
import { access } from 'fs/promises';
|
||||
|
||||
export default async () => {
|
||||
let IMMICH_TEST_ASSET_PATH: string = '';
|
||||
|
||||
if (process.env.IMMICH_TEST_ASSET_PATH === undefined) {
|
||||
IMMICH_TEST_ASSET_PATH = path.normalize(`${__dirname}/../../../server/test/assets/`);
|
||||
process.env.IMMICH_TEST_ASSET_PATH = IMMICH_TEST_ASSET_PATH;
|
||||
} else {
|
||||
IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
||||
}
|
||||
|
||||
const directoryExists = async (dirPath: string) =>
|
||||
await access(dirPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) {
|
||||
throw new Error(
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.DB_HOSTNAME === undefined) {
|
||||
// DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container.
|
||||
const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.1.11')
|
||||
.withExposedPorts(5432)
|
||||
.withDatabase('immich')
|
||||
.withUsername('postgres')
|
||||
.withPassword('postgres')
|
||||
.withReuse()
|
||||
.start();
|
||||
|
||||
process.env.DB_URL = pg.getConnectionUri();
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.IMMICH_TEST_ENV = 'true';
|
||||
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../../../server/test/e2e/immich-e2e-config.json`);
|
||||
process.env.TZ = 'Z';
|
||||
};
|
||||
84
cli/test/e2e/upload.e2e-spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { api } from '@test/api';
|
||||
import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from 'immich/test/test-utils';
|
||||
import { LoginResponseDto } from 'src/api/open-api';
|
||||
import Upload from 'src/commands/upload';
|
||||
import { APIKeyCreateResponseDto } from '@app/domain';
|
||||
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
|
||||
|
||||
describe(`upload (e2e)`, () => {
|
||||
let server: any;
|
||||
let admin: LoginResponseDto;
|
||||
let apiKey: APIKeyCreateResponseDto;
|
||||
spyOnConsole();
|
||||
|
||||
beforeAll(async () => {
|
||||
server = (await testApp.create()).getHttpServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken);
|
||||
process.env.IMMICH_API_KEY = apiKey.secret;
|
||||
});
|
||||
|
||||
it('should upload a folder recursively', async () => {
|
||||
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(assets.length).toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
it('should not create a new album', async () => {
|
||||
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
|
||||
const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
|
||||
expect(albums.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should create album from folder name', async () => {
|
||||
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
album: true,
|
||||
});
|
||||
|
||||
const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
|
||||
expect(albums.length).toEqual(1);
|
||||
const natureAlbum = albums[0];
|
||||
expect(natureAlbum.albumName).toEqual('nature');
|
||||
});
|
||||
|
||||
it('should add existing assets to album', async () => {
|
||||
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Upload again, but this time add to album
|
||||
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
album: true,
|
||||
});
|
||||
|
||||
const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
|
||||
expect(albums.length).toEqual(1);
|
||||
const natureAlbum = albums[0];
|
||||
expect(natureAlbum.albumName).toEqual('nature');
|
||||
});
|
||||
|
||||
it('should upload to the specified album name', async () => {
|
||||
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
albumName: 'testAlbum',
|
||||
});
|
||||
|
||||
const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
|
||||
expect(albums.length).toEqual(1);
|
||||
const testAlbum = albums[0];
|
||||
expect(testAlbum.albumName).toEqual('testAlbum');
|
||||
});
|
||||
});
|
||||
3
cli/test/global-setup.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
};
|
||||
@@ -8,17 +8,24 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"target": "es2021",
|
||||
"moduleResolution": "node16",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"rootDirs": ["src", "../server/src"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@test": ["test"],
|
||||
"@test/*": ["test/*"]
|
||||
"@test": ["../server/test"],
|
||||
"@test/*": ["../server/test/*"],
|
||||
"@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/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["dist", "node_modules", "upload"]
|
||||
|
||||
@@ -12,6 +12,7 @@ x-server-build: &server-common
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
target: dev
|
||||
restart: always
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||
@@ -19,8 +20,6 @@ x-server-build: &server-common
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
@@ -29,7 +28,7 @@ x-server-build: &server-common
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
command: npm run start:debug immich
|
||||
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 3001:3001
|
||||
@@ -37,11 +36,10 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: npm run start:debug microservices
|
||||
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
|
||||
<<: *server-common
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
@@ -51,7 +49,6 @@ services:
|
||||
depends_on:
|
||||
- database
|
||||
- immich-server
|
||||
- typesense
|
||||
|
||||
immich-web:
|
||||
container_name: immich_web
|
||||
@@ -59,7 +56,7 @@ services:
|
||||
build:
|
||||
context: ../web
|
||||
dockerfile: Dockerfile
|
||||
command: npm run dev --host
|
||||
command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000"
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
@@ -89,30 +86,17 @@ services:
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
depends_on:
|
||||
- database
|
||||
restart: unless-stopped
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
environment:
|
||||
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||
- TYPESENSE_DATA_DIR=/data
|
||||
# remove this to get debug messages
|
||||
- GLOG_minloglevel=1
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/typesense:/data
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
|
||||
image: redis:6.2-alpine@sha256:b6124ab2e45cc332e16398022a411d7e37181f21ff7874835e0180f56a09e82a
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -24,7 +24,6 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
@@ -36,7 +35,6 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
- immich-server
|
||||
|
||||
immich-machine-learning:
|
||||
@@ -51,26 +49,14 @@ services:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
environment:
|
||||
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||
- TYPESENSE_DATA_DIR=/data
|
||||
# remove this to get debug messages
|
||||
- GLOG_minloglevel=1
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/typesense:/data
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
|
||||
image: redis:6.2-alpine@sha256:b6124ab2e45cc332e16398022a411d7e37181f21ff7874835e0180f56a09e82a
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -25,7 +25,6 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-microservices:
|
||||
@@ -43,7 +42,6 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-machine-learning:
|
||||
@@ -55,26 +53,14 @@ services:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
environment:
|
||||
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||
- TYPESENSE_DATA_DIR=/data
|
||||
# remove this to get debug messages
|
||||
- GLOG_minloglevel=1
|
||||
volumes:
|
||||
- tsdata:/data
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:80cc8518800438c684a53ed829c621c94afd1087aaeb59b0d4343ed3e7bcf6c5
|
||||
image: redis:6.2-alpine@sha256:b6124ab2e45cc332e16398022a411d7e37181f21ff7874835e0180f56a09e82a
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:50d9be76e9a90da4c781554955e0ffc79d9d5c4226838e64b36aacc97cbc35ad
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -88,4 +74,3 @@ services:
|
||||
volumes:
|
||||
pgdata:
|
||||
model-cache:
|
||||
tsdata:
|
||||
|
||||
@@ -6,8 +6,7 @@ UPLOAD_LOCATION=./library
|
||||
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
|
||||
IMMICH_VERSION=release
|
||||
|
||||
# Connection secrets for postgres and typesense. You should change these to random passwords
|
||||
TYPESENSE_API_KEY=some-random-text
|
||||
# Connection secret for postgres. You should change it to a random password
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# The values below this line do not need to be changed
|
||||
|
||||
@@ -11,7 +11,6 @@ services:
|
||||
# volumes:
|
||||
# - /usr/lib/wsl:/usr/lib/wsl # If using VAAPI in WSL2
|
||||
# environment:
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all # If using NVIDIA GPU
|
||||
# - LD_LIBRARY_PATH=/usr/lib/wsl/lib # If using VAAPI in WSL2
|
||||
# - LIBVA_DRIVER_NAME=d3d12 # If using VAAPI in WSL2
|
||||
# deploy: # Uncomment this section if using NVIDIA GPU
|
||||
|
||||
70
docs/blog/2023/2023-recap.mdx
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Immich Recap 2023
|
||||
authors: [alextran]
|
||||
tags: [update, recap-2023]
|
||||
---
|
||||
|
||||
Hi everyone,
|
||||
|
||||
Alex from Immich here.
|
||||
|
||||
We are entering the last few weeks of 2023, and it has been quite a year for Immich. The project has grown so much in terms of users, developers, features, maturity, and the community around it. When I started working on Immich, it was simply a challenge for myself and an opportunity to learn new technologies, crafting something fun and useful for my wife during my free time to satisfy my urge to build and create things. I never thought it would become so popular and help so many people. At the end of the day, all we have is memory. I am proud that the team and I have created something to make storing and viewing those precious memories easier without restrictions and without sacrificing our privacy. As the year closes, here’s a recap of everything the project accomplished in 2023.
|
||||
|
||||
# Milestones
|
||||
|
||||
- Public shared links
|
||||
- Favorites page
|
||||
- Immich turned 1
|
||||
- Material Design 3 on the mobile app
|
||||
- Auto-link LivePhotos server-side
|
||||
- iOS background backup
|
||||
- Explore page
|
||||
- CLIP search
|
||||
- Search by metadata
|
||||
- Responsive web app
|
||||
- Archive page
|
||||
- Asset descriptions
|
||||
- 10,000 stars on GitHub
|
||||
- Manage auth devices
|
||||
- Map view
|
||||
- Facial recognition, clustering, searching, renaming, and person management
|
||||
- Partner sharing and unifying timeline between partners' users
|
||||
- Custom storage label
|
||||
- XMP sidecar reading
|
||||
- RAW file formats
|
||||
- Justified layout on the web
|
||||
- Memories
|
||||
- Multi-select via SHIFT
|
||||
- Android Motion Photos
|
||||
- 360° Photos
|
||||
- Album description
|
||||
- Album performance improvements (time buckets)
|
||||
- Video hardware transcoding
|
||||
- Slideshow mode on the web
|
||||
- Configuration file
|
||||
- External libraries
|
||||
- Trash page
|
||||
- Custom theme
|
||||
- Asset Stacking
|
||||
- 20,000 stars on GitHub
|
||||
- Shared album activity and comments
|
||||
- CLI v2
|
||||
- Down to 5 containers (from 8)
|
||||
|
||||
# Fun Statistics
|
||||
|
||||
- We have gone from the release version `1.41.0` to `1.90.0` at the time of writing. On average, we see a release every 7 days.
|
||||
- According to GitHub's metrics, the `immich-server` container image has been pulled almost _4 million_ times.
|
||||
- According to mobile app store metrics, we have 22,000 installations on Android and 6700 installation units on iOS (opt-in only).
|
||||
- Immich is making around $1200/month on average from donations. (Thank you all so much!)
|
||||
- We were guests on two podcasts:
|
||||
- [Self-hosted](https://selfhosted.show/110)
|
||||
- [The Vergecast](https://www.theverge.com/23938533/self-hosting-local-first-software-vergecast)
|
||||
- There are over 4,500 members on the Discord server.
|
||||
- We have over 22,000 stars on the main GitHub repository, gaining 15,000 stars since January 2023.
|
||||
|
||||
Diving into the next year, the team will continue to build on the foundation we have laid out over the past year, implementing more advanced features for searching, organizing, and sharing between users. Bugs will continue to be squashed and conquered. “Shit Alex wrote'' code will continue to be replaced by beautiful, clean code from Jason, Zack, Boet, Daniel, Osorin, Mert, Fynn, Marty, Martin, and Jonathan. The team has my eternal gratitude for creating a welcoming environment for new contributors, helping, teaching, and learning from each other. I’ve realized that hardly a day has gone by where the team hasn’t been in communication about Immich related topics over the past year.
|
||||
|
||||
My long-term goal is to help hone Immich into a diamond in the FOSS space, where the UI, UX, development experiences, documentation, and quality are at a high standard while remaining free for everybody to use.
|
||||
|
||||
I hope you enjoy Immich and have a happy and peaceful holiday.
|
||||
@@ -26,11 +26,10 @@ Immich optionally uses machine learning for several features. However, it can be
|
||||
|
||||
### How can I lower Immich's CPU usage?
|
||||
|
||||
The initial backup is the most intensive due to the number of jobs running. The most CPU-intensive ones are transcoding and machine learning jobs (Tag Images, Encode CLIP, Recognize Faces), and to a lesser extent thumbnail generation. Here are some ways to lower their CPU usage:
|
||||
The initial backup is the most intensive due to the number of jobs running. The most CPU-intensive ones are transcoding and machine learning jobs (Tag Images, Smart Search, Recognize Faces), and to a lesser extent thumbnail generation. Here are some ways to lower their CPU usage:
|
||||
|
||||
- Lower the job concurrency for these jobs to 1.
|
||||
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
|
||||
- Set the `TYPESENSE_THREAD_POOL_SIZE` environmental variable and restart the Typesense container. For instance, `TYPESENSE_THREAD_POOL_SIZE=8` will limit it to 8 threads.
|
||||
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
|
||||
- You _must_ re-run the Recognize Faces job for all images after this for facial recognition on new images to work properly.
|
||||
- If these changes are not enough, see [below](/docs/FAQ.md#how-can-i-disable-machine-learning) for how you can disable machine learning.
|
||||
@@ -45,14 +44,6 @@ Machine learning can be disabled under Settings > Machine Learning Settings, eit
|
||||
|
||||
However, disabling all jobs will not disable the machine learning service itself. To prevent it from starting up at all in this case, you can comment out the `immich-machine-learning` section of the docker-compose.yml.
|
||||
|
||||
### How can I disable TypeSense?
|
||||
|
||||
:::info
|
||||
Disabling Typesense will result in a poor search experience since searching is reliant on it.
|
||||
:::
|
||||
|
||||
You can disable Typesense by commenting out the `immich-typesense` section of the docker-compose.yml and setting `TYPESENSE_ENABLED=false` in your .env file.
|
||||
|
||||
### I'm getting errors about models being corrupt or failing to download. What do I do?
|
||||
|
||||
You can delete the model cache volume, which is where models are downloaded. This will give the service a clean environment to download the model again.
|
||||
@@ -65,9 +56,9 @@ Template changes will only apply to new assets. To retroactively apply the templ
|
||||
|
||||
This is fixed by running the storage migration job.
|
||||
|
||||
### Why is object detection not very good?
|
||||
### Why are there so many thumbnail generation jobs?
|
||||
|
||||
The default image tagging model is relatively small. You can change this for a larger model like `google/vit-base-patch16-224` by setting the model name under Settings > Machine Learning Settings > Image Tagging. You can then re-run the Image Tagging job to get improved tags.
|
||||
Immich generates three thumbnails for each asset (blurred, small, and large), as well as a thumbnail for each recognized face.
|
||||
|
||||
### How can I see Immich logs?
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ services:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
SCHEDULE: "@daily"
|
||||
BACKUP_NUM_KEEP: 7
|
||||
BACKUP_DIR: /db_dumps
|
||||
volumes:
|
||||
- ./db_dumps:/db_dumps
|
||||
|
||||
@@ -28,3 +28,13 @@ server {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caddy example config
|
||||
|
||||
As an alternative to nginx, you can also use [Caddy](https://caddyserver.com/) as a reverse proxy (with automatic HTTPS configuration). Below is an example config.
|
||||
|
||||
```
|
||||
immich.example.org {
|
||||
reverse_proxy http://<snip>:2283
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
import AppArchitecture from './img/app-architecture.png';
|
||||
|
||||
# Architecture
|
||||
|
||||
Immich uses a traditional client-server design, with a dedicated database for data persistence. The frontend clients communicate with backend services over HTTP using REST APIs.
|
||||
Immich uses a traditional client-server design, with a dedicated database for data persistence. The frontend clients communicate with backend services over HTTP using REST APIs. Below is a high level diagram of the architecture.
|
||||
|
||||
## High Level Diagram
|
||||
|
||||

|
||||
<img alt="Immich Architecture" src={AppArchitecture} className="p-4 dark:bg-immich-dark-primary my-4" />
|
||||
|
||||
The diagram shows clients communicating with the server via REST, as well as the flow of database between backend services.
|
||||
The diagram shows clients communicating with the server's API via REST. The server communicates with downstream systems (i.e. Redis, Postgres, Machine Learning, file system) through repository interfaces. Not shown in the diagram, is that the server is split into two separate containers `immich-server` and `immich-microservices`. The microservices container does not handle API requests or schedule cron jobs, but primarily handles incoming job requests from Redis.
|
||||
|
||||
## Clients
|
||||
|
||||
@@ -45,7 +47,6 @@ The Immich backend is divided into several services, which are run as individual
|
||||
1. `immich-machine-learning` - Execute machine learning models
|
||||
1. `postgres` - Persistent data storage
|
||||
1. `redis`- Queue management for `immich-microservices`
|
||||
1. `typesense`- Specialized database for search, specifically with vector comparison features
|
||||
|
||||
### Immich Server
|
||||
|
||||
@@ -72,10 +73,9 @@ The Immich Microservices image uses the same `Dockerfile` as the Immich Server,
|
||||
- Thumbnail Generation
|
||||
- Metadata Extraction
|
||||
- Video Transcoding
|
||||
- Object Tagging
|
||||
- Smart Search
|
||||
- Facial Recognition
|
||||
- Storage Template Migration
|
||||
- Search (Typesense synchronization)
|
||||
- Sidecar (see [XMP Sidecars](/docs/features/xmp-sidecars.md))
|
||||
- Background jobs (file deletion, user deletion)
|
||||
|
||||
@@ -108,9 +108,3 @@ See [Database Migrations](./database-migrations.md) for more information about h
|
||||
### Redis
|
||||
|
||||
Immich uses [Redis](https://redis.com/) via [BullMQ](https://docs.bullmq.io/) to manage job queues. Some jobs trigger subsequent jobs. For example, object detection relies on thumbnail generation and automatically run after one is generated.
|
||||
|
||||
### Typesense
|
||||
|
||||
Immich synchronizes some of the Postgres data into Typesense, so it can execute vector related queries in order to implement certain features including, facial recognition and CLIP search.
|
||||
|
||||
<!-- - [NGINX](https://www.nginx.com/) for internal communication between containers and load balancing when scaling. -->
|
||||
132
docs/docs/developer/img/app-architecture.drawio.xml
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" modified="2023-12-12T20:57:14.605Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" etag="df4JlXsRZhazQ-YEVHpD" version="22.1.7" type="google">
|
||||
<diagram name="Page-1" id="B4lqvvtjK-gtNbJBlj3M">
|
||||
<mxGraphModel dx="1434" dy="798" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-8" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.polygon;polyCoords=[[0.25,0],[0.75,0],[1,0.25],[1,0.75],[0.75,1],[0.25,1],[0,0.75],[0,0.25]];polyline=0;fillColor=#d0cee2;strokeColor=#56517e;strokeWidth=16;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="187" y="28" width="480" height="480" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-57" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.167;entryDx=0;entryDy=0;strokeWidth=3;endArrow=block;endFill=1;entryPerimeter=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-11" target="skFinV3TG75uy4JOHTMt-30">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-11" value="Web App<br>(SvelteKit)" style="whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="24" y="88" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-58" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;endArrow=block;endFill=1;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-12" target="skFinV3TG75uy4JOHTMt-30">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-12" value="Mobile App<br>(Flutter)" style="whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="24" y="230" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-59" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.013;entryY=0.863;entryDx=0;entryDy=0;strokeWidth=3;endArrow=block;endFill=1;entryPerimeter=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-13" target="skFinV3TG75uy4JOHTMt-30">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-13" value="Immich CLI<br>(TypeScript)" style="whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="24" y="368" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-23" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.polygon;polyCoords=[[0.25,0],[0.75,0],[1,0.25],[1,0.75],[0.75,1],[0.25,1],[0,0.75],[0,0.25]];polyline=0;fillColor=#b1ddf0;strokeColor=#10739e;strokeWidth=2;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="335" y="183" width="170" height="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-28" value="<h1>Server</h1>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="377" y="28" width="100" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-116" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=block;endFill=1;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-30" target="skFinV3TG75uy4JOHTMt-23">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="325" y="268" />
|
||||
<mxPoint x="325" y="268" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-30" value="API <br>Controllers" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b0e3e6;strokeColor=#0e8088;arcSize=0;strokeWidth=1;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;" vertex="1" parent="1">
|
||||
<mxGeometry x="215" y="170" width="95" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-43" value="<h2><span style="background-color: initial; font-size: 12px; font-weight: normal;">Repositories</span></h2><b>"Infra"<br></b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b0e3e6;strokeColor=#0e8088;arcSize=0;strokeWidth=1;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;" vertex="1" parent="1">
|
||||
<mxGeometry x="543" y="169" width="100" height="198" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-45" value="Cron" style="rhombus;whiteSpace=wrap;html=1;fillColor=#b0e3e6;strokeColor=#0e8088;strokeWidth=1;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="270" y="388" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-46" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.625;exitY=0.15;exitDx=0;exitDy=0;strokeWidth=3;endArrow=block;endFill=1;entryX=0.106;entryY=0.847;entryDx=0;entryDy=0;entryPerimeter=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;exitPerimeter=0;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-45" target="skFinV3TG75uy4JOHTMt-23">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-52" value="Postgres" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#dae8fc;strokeColor=#6c8ebf;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="745" y="270" width="60" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-128" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0;exitDx=0;exitDy=52.5;exitPerimeter=0;strokeColor=#000000;strokeWidth=3;endArrow=block;endFill=1;fillColor=#fad9d5;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-60" target="skFinV3TG75uy4JOHTMt-131">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="620" y="350" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-60" value="Redis" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="745" y="380" width="60" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-67" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;edgeStyle=orthogonalEdgeStyle;strokeWidth=1;dashed=1;endArrow=none;endFill=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fillColor=#ffff88;strokeColor=#000000;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-74" target="skFinV3TG75uy4JOHTMt-84">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="166" y="18.99000000000001" as="sourcePoint" />
|
||||
<mxPoint x="165" y="508" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-69" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=block;endFill=1;startArrow=none;startFill=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-43" target="skFinV3TG75uy4JOHTMt-52">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="645" y="168" as="sourcePoint" />
|
||||
<mxPoint x="755" y="168" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-74" value="<i style="font-size: 10px;">HTTP (OpenAPI)</i>" style="whiteSpace=wrap;html=1;fillColor=#eeeeee;strokeColor=#36393d;fontSize=10;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="110" y="48" width="105" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-78" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=block;startFill=1;endArrow=none;endFill=0;strokeWidth=3;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-43" target="skFinV3TG75uy4JOHTMt-23">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-84" value="<i style="font-size: 10px;">HTTP (OpenAPI)</i>" style="whiteSpace=wrap;html=1;fillColor=#eeeeee;strokeColor=#36393d;fontSize=10;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="110" y="468" width="105" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-85" value="<h2>Core<br>Services</h2><div><b>"Domain"</b></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="370" y="208" width="100" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-87" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=block;endFill=1;startArrow=none;startFill=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-43" target="skFinV3TG75uy4JOHTMt-94">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="635" y="268" as="sourcePoint" />
|
||||
<mxPoint x="735" y="398" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-94" value="Machine Learning<br>(Python)" style="whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="705" y="48" width="120" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-97" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=block;endFill=1;startArrow=none;startFill=0;entryX=0;entryY=1;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-43" target="skFinV3TG75uy4JOHTMt-101">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="655" y="318" as="sourcePoint" />
|
||||
<mxPoint x="785" y="298" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-101" value="File<br>System" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontColor=#333333;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="735" y="163" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-117" value="<i style="font-size: 10px;">HTTP</i>" style="whiteSpace=wrap;html=1;fillColor=#eeeeee;strokeColor=#36393d;fontSize=10;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="682" y="163" width="48" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-129" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endArrow=block;endFill=1;strokeWidth=3;strokeColor=#000000;fillColor=#fad9d5;entryX=0.888;entryY=0.865;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-131" target="skFinV3TG75uy4JOHTMt-23">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="565" y="278" as="sourcePoint" />
|
||||
<mxPoint x="479" y="340" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-131" value="Jobs" style="whiteSpace=wrap;html=1;strokeWidth=2;fillColor=#b0e3e6;strokeColor=#0e8088;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="563" y="340" width="60" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-134" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;entryX=0;entryY=0;entryDx=0;entryDy=27.5;entryPerimeter=0;endArrow=block;endFill=1;startArrow=none;startFill=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;fillColor=#fad9d5;strokeColor=#000000;" edge="1" parent="1" source="skFinV3TG75uy4JOHTMt-43" target="skFinV3TG75uy4JOHTMt-60">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="634" y="318" as="sourcePoint" />
|
||||
<mxPoint x="755" y="289" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="skFinV3TG75uy4JOHTMt-140" value="<i style="font-size: 10px;">Queue</i>" style="whiteSpace=wrap;html=1;fillColor=#eeeeee;strokeColor=#36393d;fontSize=10;fontFamily=Overpass;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DOverpass;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="679" y="394" width="48" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 55 KiB |
@@ -42,6 +42,7 @@ Usage: immich [options] [command]
|
||||
Immich command line interface
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
# Search
|
||||
|
||||
Immich uses Typesense as the primary search database to enable high performance search mechanism.
|
||||
Immich uses Postgres as its search database for both metadata and smart search.
|
||||
|
||||
Typesense is a powerful search engine that can be integrated with popular natural language processing (NLP) models like CLIP and SBERT to provide highly accurate and relevant search results. Here are some benefits of using Typesense integrated search for CLIP and SBERT:
|
||||
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like CLIP to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
|
||||
|
||||
Improved Search Accuracy: Typesense uses a combination of indexing, querying, and ranking algorithms to quickly and accurately retrieve relevant search results. When integrated with CLIP and SBERT, Typesense can leverage the semantic understanding and deep learning capabilities of these models to further improve the accuracy of search results.
|
||||
|
||||
Faster Search Response Times: Typesense is optimized for lightning-fast search response times, making it ideal for applications that require near-instantaneous search results. By integrating with CLIP and SBERT, Typesense can reduce the time required to process complex search queries, making it even faster and more efficient.
|
||||
|
||||
Enhanced Semantic Search Capabilities: CLIP and SBERT are powerful NLP models that can extract the semantic meaning from text, enabling more nuanced search queries. By integrating with Typesense, these models can help to improve the accuracy of semantic search, enabling users to find the most relevant results based on the true meaning of their query.
|
||||
|
||||
Greater Search Flexibility: Typesense provides flexible search capabilities, including fuzzy search, partial search, enabling users to find the information they need quickly and easily. When integrated with CLIP and SBERT, Typesense can offer even greater flexibility, allowing users to refine their search queries using natural language and providing more accurate and relevant results.
|
||||
|
||||
(Generated by Chat-GPT4)
|
||||
Metadata search (prefixed with `m:`) can search specifically by text without the use of a model.
|
||||
|
||||
Some search examples:
|
||||
<img src={require('./img/search-ex-2.webp').default} title='Search Example 1' />
|
||||
|
||||
@@ -14,8 +14,6 @@ docker exec -it <id or name> <command> # attach to a container with a c
|
||||
docker exec -it immich_server sh
|
||||
docker exec -it immich_microservices sh
|
||||
docker exec -it immich_machine_learning sh
|
||||
docker exec -it immich_web sh
|
||||
docker exec -it immich_proxy sh
|
||||
```
|
||||
|
||||
## Logs
|
||||
@@ -26,8 +24,6 @@ docker logs <id or name> # see the logs for a specific container (by id
|
||||
docker logs immich_server
|
||||
docker logs immich_microservices
|
||||
docker logs immich_machine_learning
|
||||
docker logs immich_web
|
||||
docker logs immich_proxy
|
||||
```
|
||||
|
||||
:::tip Follow a log
|
||||
|
||||
100
docs/docs/guides/external-library.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# External Library
|
||||
|
||||
This guide walks you through adding an [External Library](../features/libraries#external-libraries).
|
||||
This guide assumes you are running Immich in Docker and that the files you wish to access are stored
|
||||
in a directory on the same machine.
|
||||
|
||||
# Mount the directory into the containers.
|
||||
|
||||
Edit `docker-compose.yml` to add two new mount points under `volumes:`
|
||||
|
||||
```
|
||||
immich-server:
|
||||
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:
|
||||
|
||||
```
|
||||
EXTERNAL_PATH=<your-path-here>
|
||||
```
|
||||
|
||||
On my computer, for example, I use this path:
|
||||
|
||||
```
|
||||
EXTERNAL_PATH=/home/tenino/photos
|
||||
```
|
||||
|
||||
Restart Immich.
|
||||
|
||||
```
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
# Set the External Path
|
||||
|
||||
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 **pencil** next to your user ID
|
||||
<img src={require('./img/pencil.png').default} width="50%" title="Pencil" />
|
||||
|
||||
- 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" />
|
||||
|
||||
- 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_
|
||||
<img src={require('./img/add-path-button.png').default} width="50%" title="Add Path button" />
|
||||
|
||||
- Enter **.** as the path and click Add
|
||||
<img src={require('./img/add-path-field.png').default} width="50%" title="Add Path field" />
|
||||
|
||||
- Save the new path
|
||||
<img src={require('./img/path-save.png').default} width="50%" title="Path Save button" />
|
||||
|
||||
- Click the three-dots menu and select **Scan New Library Files**
|
||||
<img src={require('./img/scan-new-library-files.png').default} width="50%" title="Scan New Library Files menu option" />
|
||||
|
||||
# Confirm stuff is happening
|
||||
|
||||
- Click **Administration**
|
||||
<img src={require('./img/administration-link.png').default} width="50%" title="Administration link" />
|
||||
|
||||
- Select the **Jobs** tab
|
||||
<img src={require('./img/jobs-tab.png').default} width="50%" title="Jobs tab" />
|
||||
|
||||
- You should see non-zero Active jobs for
|
||||
Library, Generate Thumbnails, and Extract Metadata.
|
||||
<img src={require('./img/job-status.png').default} width="50%" title="Job Status display" />
|
||||
BIN
docs/docs/guides/img/account-settings.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/docs/guides/img/add-path-button.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
docs/docs/guides/img/add-path-field.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/docs/guides/img/administration-link.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/docs/guides/img/create-external-library-button.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
docs/docs/guides/img/edit-import-paths.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/docs/guides/img/external-path.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/docs/guides/img/job-status.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/docs/guides/img/jobs-tab.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/docs/guides/img/libraries-dropdown.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
docs/docs/guides/img/path-save.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/docs/guides/img/pencil.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
docs/docs/guides/img/scan-new-library-files.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/docs/guides/img/user-avatar.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/docs/guides/img/users-tab.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
58
docs/docs/guides/remote-access.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Remote Access
|
||||
|
||||
This page gives a few pointers on how to access your Immich instance from outside your LAN.
|
||||
|
||||
:::danger
|
||||
Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you succeptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks.
|
||||
:::
|
||||
|
||||
## Option 1: VPN to home network
|
||||
|
||||
You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/)
|
||||
|
||||
### Pros:
|
||||
|
||||
- Simple to set up and very secure.
|
||||
- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk.
|
||||
- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal.
|
||||
|
||||
### Cons:
|
||||
|
||||
- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider.
|
||||
- VPN software needs to be installed and active on both server-side and client-side.
|
||||
- Requires you to open a port on your router to your server.
|
||||
|
||||
## Option 2: Tailscale
|
||||
|
||||
If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
|
||||
|
||||
### Pros
|
||||
|
||||
- Minimal configuration needed on server and client sides.
|
||||
- You are protected against zero-day vulnerabilities on Immich.
|
||||
|
||||
### Cons
|
||||
|
||||
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
|
||||
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
|
||||
- Tailscale needs to be installed and running on both server-side and client-side.
|
||||
|
||||
## Option 3: Reverse Proxy
|
||||
|
||||
A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side.
|
||||
|
||||
If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](https://immich.app/docs/administration/reverse-proxy).
|
||||
|
||||
You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
|
||||
|
||||
A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder.
|
||||
|
||||
### Pros
|
||||
|
||||
- No additional software needs to be installed client-side
|
||||
- If you only need access to the web interface remotely, it is possible to set up access controls that shield you from zero-day vulnerabilities on Immich. [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) has a generous free tier.
|
||||
|
||||
### Cons
|
||||
|
||||
- Complex configuration
|
||||
- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out.
|
||||
@@ -32,15 +32,12 @@ The default configuration looks like this:
|
||||
"backgroundTask": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"clipEncoding": {
|
||||
"smartSearch": {
|
||||
"concurrency": 2
|
||||
},
|
||||
"metadataExtraction": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"objectTagging": {
|
||||
"concurrency": 2
|
||||
},
|
||||
"recognizeFaces": {
|
||||
"concurrency": 2
|
||||
},
|
||||
@@ -66,14 +63,13 @@ The default configuration looks like this:
|
||||
"concurrency": 1
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"enabled": true,
|
||||
"level": "log"
|
||||
},
|
||||
"machineLearning": {
|
||||
"enabled": true,
|
||||
"url": "http://immich-machine-learning:3003",
|
||||
"classification": {
|
||||
"enabled": true,
|
||||
"modelName": "microsoft/resnet-50",
|
||||
"minScore": 0.9
|
||||
},
|
||||
"clip": {
|
||||
"enabled": true,
|
||||
"modelName": "ViT-B-32__openai"
|
||||
@@ -88,11 +84,11 @@ The default configuration looks like this:
|
||||
},
|
||||
"map": {
|
||||
"enabled": true,
|
||||
"tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
"lightStyle": "",
|
||||
"darkStyle": ""
|
||||
},
|
||||
"reverseGeocoding": {
|
||||
"enabled": true,
|
||||
"citiesFileOverride": "cities500"
|
||||
"enabled": true
|
||||
},
|
||||
"oauth": {
|
||||
"enabled": false,
|
||||
@@ -134,9 +130,6 @@ The default configuration looks like this:
|
||||
"enabled": true,
|
||||
"cronExpression": "0 0 * * *"
|
||||
}
|
||||
},
|
||||
"stylesheets": {
|
||||
"css": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
---
|
||||
sidebar_position: 30
|
||||
---
|
||||
|
||||
# Docker Compose [Recommended]
|
||||
|
||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||
|
||||
### Step 1 - Download the required files
|
||||
|
||||
Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
|
||||
|
||||
```bash title="Move to the directory you created"
|
||||
mkdir ./immich-app
|
||||
cd ./immich-app
|
||||
```
|
||||
|
||||
Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file], either by running the following commands:
|
||||
|
||||
```bash title="Get docker-compose.yml file"
|
||||
wget https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
```
|
||||
|
||||
```bash title="Get .env file"
|
||||
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
```
|
||||
|
||||
```bash title="(Optional) Get hwaccel.yml file"
|
||||
wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
```
|
||||
|
||||
or by downloading from your browser and moving the files to the directory that you created.
|
||||
|
||||
Note: If you downloaded the files from your browser, also ensure that you rename `example.env` to `.env`.
|
||||
|
||||
:::info
|
||||
Optionally, you can use the [`hwaccel.yml`][hw-file] file to enable hardware acceleration for transcoding. See the [Hardware Transcoding](/docs/features/hardware-transcoding.md) guide for info on how to set this up.
|
||||
:::
|
||||
|
||||
### Step 2 - Populate the .env file with custom values
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example <code>.env</code> content</summary>
|
||||
|
||||
```bash
|
||||
###################################################################################
|
||||
# Database
|
||||
###################################################################################
|
||||
|
||||
DB_HOSTNAME=immich_postgres
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_DATABASE_NAME=immich
|
||||
|
||||
# Optional Database settings:
|
||||
# DB_PORT=5432
|
||||
|
||||
###################################################################################
|
||||
# Redis
|
||||
###################################################################################
|
||||
|
||||
REDIS_HOSTNAME=immich_redis
|
||||
|
||||
# Optional Redis settings:
|
||||
|
||||
# Note: these parameters are not automatically passed to the Redis Container
|
||||
# to do so, please edit the docker-compose.yml file as well. Redis is not configured
|
||||
# via environment variables, only redis.conf or the command line
|
||||
|
||||
# REDIS_PORT=6379
|
||||
# REDIS_DBINDEX=0
|
||||
# REDIS_PASSWORD=
|
||||
# REDIS_SOCKET=
|
||||
|
||||
###################################################################################
|
||||
# Upload File Location
|
||||
#
|
||||
# This is the location where uploaded files are stored.
|
||||
###################################################################################
|
||||
|
||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||
|
||||
|
||||
###################################################################################
|
||||
# Log message level - [simple|verbose]
|
||||
###################################################################################
|
||||
|
||||
LOG_LEVEL=simple
|
||||
|
||||
###################################################################################
|
||||
# Typesense
|
||||
###################################################################################
|
||||
# TYPESENSE_ENABLED=false
|
||||
TYPESENSE_API_KEY=some-random-text
|
||||
# TYPESENSE_HOST: typesense
|
||||
# TYPESENSE_PORT: 8108
|
||||
# TYPESENSE_PROTOCOL: http
|
||||
|
||||
###################################################################################
|
||||
# Reverse Geocoding
|
||||
#
|
||||
# Reverse geocoding is done locally which has a small impact on memory usage
|
||||
# This memory usage can be altered by changing the REVERSE_GEOCODING_PRECISION variable
|
||||
# This ranges from 0-3 with 3 being the most precise
|
||||
# 3 - Cities > 500 population: ~200MB RAM
|
||||
# 2 - Cities > 1000 population: ~150MB RAM
|
||||
# 1 - Cities > 5000 population: ~80MB RAM
|
||||
# 0 - Cities > 15000 population: ~40MB RAM
|
||||
####################################################################################
|
||||
|
||||
# DISABLE_REVERSE_GEOCODING=false
|
||||
# REVERSE_GEOCODING_PRECISION=3
|
||||
|
||||
####################################################################################
|
||||
# WEB - Optional
|
||||
#
|
||||
# Custom message on the login page, should be written in HTML form.
|
||||
# For example:
|
||||
# PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||
####################################################################################
|
||||
|
||||
PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
|
||||
|
||||
###################################################################################
|
||||
# Immich Version - Optional
|
||||
#
|
||||
# This allows all immich docker images to be pinned to a specific version. By default,
|
||||
# the version is "release" but could be a specific version, like "v1.59.0".
|
||||
###################################################################################
|
||||
|
||||
#IMMICH_VERSION=
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- Populate custom database information if necessary.
|
||||
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
|
||||
- Consider changing `DB_PASSWORD` to something randomly generated
|
||||
- Consider changing `TYPESENSE_API_KEY` to something randomly generated
|
||||
|
||||
### Step 3 - Start the containers
|
||||
|
||||
From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker-compose up -d`.
|
||||
|
||||
```bash title="Start the containers using docker compose command"
|
||||
docker-compose up -d # or `docker compose up -d` based on your docker-compose version
|
||||
```
|
||||
|
||||
:::tip
|
||||
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Note that downloading container images might require you to authenticate to the GitHub Container Registry ([steps here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry)).
|
||||
:::
|
||||
|
||||
### Step 4 - Upgrading
|
||||
|
||||
If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
|
||||
|
||||
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||
|
||||
```bash title="Upgrade Immich"
|
||||
docker-compose pull && docker-compose up -d # Or `docker compose up -d`
|
||||
```
|
||||
|
||||
:::caution Automatic Updates
|
||||
Immich is currently under heavy development, which means you can expect breaking changes and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
|
||||
:::
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
102
docs/docs/install/docker-compose.mdx
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
sidebar_position: 30
|
||||
---
|
||||
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
|
||||
|
||||
# Docker Compose [Recommended]
|
||||
|
||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||
|
||||
### Step 1 - Download the required files
|
||||
|
||||
Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
|
||||
|
||||
```bash title="Move to the directory you created"
|
||||
mkdir ./immich-app
|
||||
cd ./immich-app
|
||||
```
|
||||
|
||||
Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file], either by running the following commands:
|
||||
|
||||
```bash title="Get docker-compose.yml file"
|
||||
wget https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
```
|
||||
|
||||
```bash title="Get .env file"
|
||||
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
```
|
||||
|
||||
```bash title="(Optional) Get hwaccel.yml file"
|
||||
wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
```
|
||||
|
||||
or by downloading from your browser and moving the files to the directory that you created.
|
||||
|
||||
Note: If you downloaded the files from your browser, also ensure that you rename `example.env` to `.env`.
|
||||
|
||||
:::info
|
||||
Optionally, you can use the [`hwaccel.yml`][hw-file] file to enable hardware acceleration for transcoding. See the [Hardware Transcoding](/docs/features/hardware-transcoding.md) guide for info on how to set this up.
|
||||
:::
|
||||
|
||||
### Step 2 - Populate the .env file with custom values
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Example <code>.env</code> content
|
||||
</summary>
|
||||
<CodeBlock language="bash">{ExampleEnv}</CodeBlock>
|
||||
</details>
|
||||
|
||||
- Populate custom database information if necessary.
|
||||
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
|
||||
- Consider changing `DB_PASSWORD` to something randomly generated
|
||||
|
||||
### Step 3 - Start the containers
|
||||
|
||||
From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker compose up -d`.
|
||||
|
||||
```bash title="Start the containers using docker compose command"
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository).
|
||||
|
||||
Note that the correct command really is `docker compose`, not `docker-compose`. If you try the latter on vanilla Ubuntu 22.04 it will fail in a different way:
|
||||
|
||||
```
|
||||
The Compose file './docker-compose.yml' is invalid because:
|
||||
'name' does not match any of the regexes: '^x-'
|
||||
```
|
||||
|
||||
See the previous paragraph about installing from the official docker repository.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Note that downloading container images might require you to authenticate to the GitHub Container Registry ([steps here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry)).
|
||||
:::
|
||||
|
||||
### Step 4 - Upgrading
|
||||
|
||||
If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
|
||||
|
||||
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||
|
||||
```bash title="Upgrade Immich"
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
:::caution Automatic Updates
|
||||
Immich is currently under heavy development, which means you can expect breaking changes and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
|
||||
:::
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
@@ -17,10 +17,10 @@ If this should not work, try running `docker compose up -d --force-recreate`.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------- | :-------------------- | :-------: | :------------------------------------------------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning, web, proxy, typesense |
|
||||
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------- | :-------------------- | :-------: | :-------------------------------------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning, web, proxy |
|
||||
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -30,14 +30,15 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
|
||||
## General
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :-------------------------- | :------------------------------------------- | :----------: | :------------------------------------------- |
|
||||
| `TZ` | Timezone | | microservices |
|
||||
| `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 |
|
||||
| `PUBLIC_LOGIN_PAGE_MESSAGE` | Public Login Page Message | | web |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server |
|
||||
| Variable | Description | Default | Services |
|
||||
| :-------------------------- | :------------------------------------------- | :-----------------: | :------------------------------------------- |
|
||||
| `TZ` | Timezone | | microservices |
|
||||
| `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 |
|
||||
| `PUBLIC_LOGIN_PAGE_MESSAGE` | Public Login Page Message | | web |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server |
|
||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www'` | server |
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -124,51 +125,6 @@ Redis (Sentinel) URL example JSON before encoding:
|
||||
}
|
||||
```
|
||||
|
||||
## Typesense
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :------------------- | :----------------------- | :---------: | :------------------------------- |
|
||||
| `TYPESENSE_ENABLED` | Enable Typesense | | server, microservices |
|
||||
| `TYPESENSE_URL` | Typesense URL | | server, microservices |
|
||||
| `TYPESENSE_HOST` | Typesense Host | `typesense` | server, microservices |
|
||||
| `TYPESENSE_PORT` | Typesense Port | `8108` | server, microservices |
|
||||
| `TYPESENSE_PROTOCOL` | Typesense Protocol | `http` | server, microservices |
|
||||
| `TYPESENSE_API_KEY` | Typesense API Key | | server, microservices, typesense |
|
||||
| `TYPESENSE_DATA_DIR` | Typesense Data Directory | `/data` | typesense |
|
||||
|
||||
:::info
|
||||
|
||||
`TYPESENSE_URL` must start with `ha://` and then include a `base64` encoded JSON string for the configuration.
|
||||
|
||||
`TYPESENSE_ENABLED`: Anything other than `false`, behaves as `true`.
|
||||
Even undefined is treated as `true`.
|
||||
|
||||
- When `TYPESENSE_URL` is defined, the other typesense (`TYPESENSE_*`) variables are ignored.
|
||||
|
||||
:::
|
||||
|
||||
Typesense URL example JSON before encoding:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"host": "typesense-1.example.net",
|
||||
"port": "443",
|
||||
"protocol": "https"
|
||||
},
|
||||
{
|
||||
"host": "typesense-2.example.net",
|
||||
"port": "443",
|
||||
"protocol": "https"
|
||||
},
|
||||
{
|
||||
"host": "typesense-3.example.net",
|
||||
"port": "443",
|
||||
"protocol": "https"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Machine Learning
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
|
||||
@@ -18,7 +18,4 @@ search home.lan
|
||||
nameserver 192.168.1.1
|
||||
```
|
||||
|
||||
When you encounter this bug, it will cause the immich-microservices to crash on startup because it cannot download
|
||||
the geocoder data. This can be solved in one of two ways: Either reconfigure your nodes to remove the searchdomain from
|
||||
`resolv.conf`, or set the `DISABLE_REVERSE_GEOCODING` environment variable for Immich to `true` to disable the geocoder.
|
||||
:::
|
||||
|
||||
@@ -5,7 +5,7 @@ sidebar_position: 20
|
||||
# Install Script [Experimental]
|
||||
|
||||
:::caution
|
||||
This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.md).
|
||||
This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.mdx).
|
||||
:::
|
||||
|
||||
In the shell, from a directory of your choice, run the following command:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Help Me!
|
||||
|
||||
BIN
docs/docs/overview/img/upload-button.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Logo
|
||||
|
||||
85
docs/docs/overview/quick-start.mdx
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
|
||||
Here is a quick, no-choices path to install Immich and take it for a test drive.
|
||||
Once you've tried it, perhaps you'll use one of the many other ways
|
||||
to install and use it.
|
||||
|
||||
## Requirements
|
||||
|
||||
Check the [requirements page](../install/requirements) to get started.
|
||||
|
||||
## Install and launch via Docker Compose
|
||||
|
||||
Follow the [Docker Compose (Recommended)](../install/docker-compose) instructions
|
||||
to install the server.
|
||||
|
||||
- Where random passwords are required, `pwgen` is a handy utility.
|
||||
- `UPLOAD_LOCATION` should be set to some new directory on the server
|
||||
with free space.
|
||||
- You may ignore "Step 4 - Upgrading".
|
||||
|
||||
## Try the Web UI
|
||||
|
||||
import RegisterAdminUser from '../partials/_register-admin.md';
|
||||
|
||||
<RegisterAdminUser />
|
||||
|
||||
Try uploading a picture from your browser.
|
||||
|
||||
<img src={require('./img/upload-button.png').default} title="Upload button" />
|
||||
|
||||
## Try the Mobile UI
|
||||
|
||||
### Download the Mobile App
|
||||
|
||||
import MobileAppDownload from '../partials/_mobile-app-download.md';
|
||||
|
||||
<MobileAppDownload />
|
||||
|
||||
### Login to the Mobile App
|
||||
|
||||
import MobileAppLogin from '../partials/_mobile-app-login.md';
|
||||
|
||||
<MobileAppLogin />
|
||||
|
||||
In the mobile app, you should see the photo you uploaded from the web UI.
|
||||
|
||||
### Transfer Photos from your Mobile Device
|
||||
|
||||
import MobileAppBackup from '../partials/_mobile-app-backup.md';
|
||||
|
||||
<MobileAppBackup />
|
||||
|
||||
Depending on how many photos are on your mobile device, this backup may
|
||||
take quite a while.
|
||||
|
||||
You can select the Jobs tab to see Immich processing your photos.
|
||||
|
||||
<img src={require('../guides/img/jobs-tab.png').default} title="Jobs tab" />
|
||||
|
||||
## Where to go from here?
|
||||
|
||||
You may decide you'd like to install the server a different way;
|
||||
the Install category on the left menu provides many options.
|
||||
|
||||
You may decide you'd like to add the _rest_ of your photos from Google Photos,
|
||||
even those not on your mobile device, via Google Takeout.
|
||||
You can use [immich-go](https://github.com/simulot/immich-go) for this.
|
||||
|
||||
You may want to
|
||||
[upload photos from your own archive](../features/command-line-interface).
|
||||
|
||||
You may want to incorporate an immutable archive of photos from an
|
||||
[External Library](../features/libraries#external-libraries);
|
||||
there's a [Guide](../guides/external-library) for that.
|
||||
|
||||
You may want your mobile device to
|
||||
[back photos up to your server automatically](../features/automatic-backup).
|
||||
|
||||
You may want to back up the content of your Immich instance
|
||||
along with other parts of your server; be sure to read about
|
||||
[database backup](../administration/backup-and-restore).
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Support The Project
|
||||
@@ -12,8 +12,8 @@ If you feel like this is the right cause and the app is something you see yourse
|
||||
|
||||
## Donation
|
||||
|
||||
- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502)
|
||||
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
|
||||
- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/immich-app)
|
||||
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/immich-app?frequency=one-time)
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
@@ -24,7 +24,7 @@ There are lots of non-monetary ways to contribute to Immich as well.
|
||||
|
||||
1. Testing - Using Immich and reporting bugs is a great way to help support the project. Found a bug? [Open an issue on GitHub][github-issue].
|
||||
1. Translations - The Immich mobile app has been translated into [17 languages][github-langs] so far! To contribute with translations, email me at alex.tran1502@gmail.com or send me a message on discord.
|
||||
1. Development - If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.md) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.md) section.
|
||||
1. Development - If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.mdx) section.
|
||||
|
||||
[github-issue]: https://github.com/immich-app/immich/issues/new/choose
|
||||
[github-langs]: https://github.com/immich-app/immich/tree/main/mobile/assets/i18n
|
||||
|
||||