Compare commits
2 commits
main
...
feat/dynam
Author | SHA1 | Date | |
---|---|---|---|
|
bdf8c9f1a9 | ||
|
57506aa1fe |
|
@ -1,20 +0,0 @@
|
|||
.vscode/
|
||||
cli/
|
||||
design/
|
||||
docker/
|
||||
docs/
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
mobile/
|
||||
|
||||
server/node_modules
|
||||
server/coverage/
|
||||
server/.reverse-geocoding-dump/
|
||||
server/upload/
|
||||
server/dist/
|
||||
|
||||
web/node_modules/
|
||||
web/coverage/
|
||||
web/.svelte-kit
|
||||
web/build/
|
4
.gitattributes
vendored
|
@ -5,8 +5,6 @@ mobile/openapi/**/*.dart linguist-generated=true
|
|||
mobile/openapi/.openapi-generator/FILES -diff -merge
|
||||
mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
||||
|
||||
mobile/lib/**/*.g.dart -diff -merge
|
||||
mobile/lib/**/*.g.dart linguist-generated=true
|
||||
|
||||
cli/src/api/open-api/**/*.md -diff -merge
|
||||
cli/src/api/open-api/**/*.md linguist-generated=true
|
||||
|
@ -17,5 +15,3 @@ 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: immich-app
|
||||
github: alextran1502
|
||||
liberapay: alex.tran1502
|
||||
custom: https://www.buymeacoffee.com/altran1502
|
||||
|
|
4
.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-13
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- name: Determine ref
|
||||
|
@ -35,7 +35,7 @@ jobs:
|
|||
with:
|
||||
ref: ${{ steps.get-ref.outputs.ref }}
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "12.x"
|
||||
|
|
3
.github/workflows/cache-cleanup.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: Cache Cleanup
|
||||
name: Clean up actions cache on PR close
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
|
@ -10,7 +10,6 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
|
|
23
.github/workflows/cli-release.yml
vendored
|
@ -1,23 +0,0 @@
|
|||
name: CLI Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./cli
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
- run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
2
.github/workflows/dispatch_sdk_update.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
|
|
16
.github/workflows/docker-cleanup.yml
vendored
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# This workflow will not trigger runs on forked repos.
|
||||
|
||||
name: Docker Cleanup
|
||||
name: Cleanup Old Docker Images
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -29,13 +29,16 @@ jobs:
|
|||
include:
|
||||
- primary-name: "immich-server"
|
||||
- primary-name: "immich-machine-learning"
|
||||
- primary-name: "immich-web"
|
||||
- primary-name: "immich-proxy"
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
|
||||
steps:
|
||||
- name: Clean temporary images
|
||||
-
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
@ -57,14 +60,17 @@ jobs:
|
|||
include:
|
||||
- primary-name: "immich-server"
|
||||
- primary-name: "immich-machine-learning"
|
||||
- primary-name: "immich-web"
|
||||
- primary-name: "immich-proxy"
|
||||
- primary-name: "immich-build-cache"
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
|
||||
steps:
|
||||
- name: Clean untagged images
|
||||
-
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.4.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.3.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
|
101
.github/workflows/docker.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: Docker
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -18,21 +18,21 @@ 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
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- context: "web"
|
||||
image: "immich-web"
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
- context: "machine-learning"
|
||||
file: "machine-learning/Dockerfile"
|
||||
image: "immich-machine-learning"
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
- context: "."
|
||||
file: "server/Dockerfile"
|
||||
image: "immich-server"
|
||||
platforms: "linux/arm64,linux/amd64"
|
||||
- context: "nginx"
|
||||
image: "immich-proxy"
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -97,10 +97,93 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
|
||||
build_and_push_server_arm_64:
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
# Prevent a failure in one image from stopping the other builds
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- context: "server"
|
||||
image: "immich-server"
|
||||
platforms: "linux/arm64,linux/amd64"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
# Workaround to fix error:
|
||||
# failed to push: failed to copy: io: read/write on closed pipe
|
||||
# See https://github.com/docker/build-push-action/issues/761
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
|
||||
name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Determine build cache output
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.file }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
|
|
5
.github/workflows/static_analysis.yml
vendored
|
@ -32,8 +32,3 @@ jobs:
|
|||
- name: Run dart analyze
|
||||
run: dart analyze --fatal-infos
|
||||
working-directory: ./mobile
|
||||
|
||||
# Enable after riverpod generator migration is completed
|
||||
# - name: Run dart custom lint
|
||||
# run: dart run custom_lint
|
||||
# working-directory: ./mobile
|
||||
|
|
58
.github/workflows/test.yml
vendored
|
@ -11,7 +11,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
name: Server (e2e)
|
||||
name: Run end-to-end test suites
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -24,7 +24,7 @@ jobs:
|
|||
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
|
||||
|
||||
doc-tests:
|
||||
name: Docs
|
||||
name: Run documentation checks
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
|
@ -45,12 +45,8 @@ jobs:
|
|||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
server-unit-tests:
|
||||
name: Server
|
||||
name: Run server unit test suites and checks
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
|
@ -80,7 +76,7 @@ jobs:
|
|||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-unit-tests:
|
||||
name: CLI
|
||||
name: Run cli test suites
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
|
@ -101,16 +97,12 @@ jobs:
|
|||
run: npm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run tsc
|
||||
run: npm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests & coverage
|
||||
run: npm run test:cov
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
web-unit-tests:
|
||||
name: Web
|
||||
name: Run web unit test suites and checks
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
|
@ -144,7 +136,7 @@ jobs:
|
|||
# if: ${{ !cancelled() }}
|
||||
|
||||
mobile-unit-tests:
|
||||
name: Mobile
|
||||
name: Run mobile unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -158,7 +150,7 @@ jobs:
|
|||
run: flutter test -j 1
|
||||
|
||||
ml-unit-tests:
|
||||
name: Machine Learning
|
||||
name: Run ML unit tests and checks
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
|
@ -174,21 +166,22 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev
|
||||
poetry run pip install --no-deps -r requirements.txt
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
poetry run ruff check --format=github app export
|
||||
poetry run ruff check --format=github app
|
||||
- name: Check black formatting
|
||||
run: |
|
||||
poetry run black --check app export
|
||||
poetry run black --check app
|
||||
- name: Run mypy type checking
|
||||
run: |
|
||||
poetry run mypy --install-types --non-interactive --strict app/ export/
|
||||
poetry run mypy --install-types --non-interactive app/
|
||||
- name: Run tests and coverage
|
||||
run: |
|
||||
poetry run pytest --cov app
|
||||
|
||||
generated-api-up-to-date:
|
||||
name: OpenAPI Clients
|
||||
name: Check generated files are up-to-date
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -209,11 +202,11 @@ jobs:
|
|||
exit 1
|
||||
|
||||
generated-typeorm-migrations-up-to-date:
|
||||
name: TypeORM Checks
|
||||
name: Check generated TypeORM migrations are up-to-date
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres@sha256:6dfee32131933ab4ca25a00360c3f427fdc134de56f9a90c6c9a4956b48aea85
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
|
@ -236,7 +229,7 @@ jobs:
|
|||
- name: Install server dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build the app
|
||||
- name: Build the
|
||||
run: npm run build
|
||||
|
||||
- name: Run existing migrations
|
||||
|
@ -252,30 +245,13 @@ jobs:
|
|||
with:
|
||||
files: |
|
||||
server/src/infra/migrations/
|
||||
- name: Verify migration files have not changed
|
||||
- name: Verify files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
echo "ERROR: Generated migration files not up to date!"
|
||||
echo "ERROR: Generated 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
|
||||
|
||||
- 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
|
||||
|
|
25
Makefile
|
@ -1,35 +1,38 @@
|
|||
dev:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
|
||||
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||
|
||||
dev-new:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||
|
||||
dev-down:
|
||||
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||
|
||||
dev-update:
|
||||
dev-new-update:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||
|
||||
dev-update:
|
||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||
|
||||
dev-scale:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||
|
||||
stage:
|
||||
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||
|
||||
pull-stage:
|
||||
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||
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
|
||||
|
||||
prod:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
|
||||
prod-scale:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||
|
||||
api:
|
||||
npm --prefix server run api:generate
|
||||
|
||||
sql:
|
||||
npm --prefix server run sql:generate
|
||||
cd ./server && npm run api:generate
|
||||
|
||||
attach-server:
|
||||
docker exec -it docker_immich-server_1 sh
|
||||
|
|
27
README.md
|
@ -2,7 +2,7 @@
|
|||
<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="License: 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"/>
|
||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<br/>
|
||||
|
@ -18,16 +18,14 @@
|
|||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</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_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## Disclaimer
|
||||
|
@ -68,7 +66,7 @@ password: demo
|
|||
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
```
|
||||
|
||||
## Features
|
||||
# Features
|
||||
|
||||
| Features | Mobile | Web |
|
||||
| -------------------------------------------- | ------ | --- |
|
||||
|
@ -87,7 +85,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
|||
| Virtual scroll | Yes | Yes |
|
||||
| OAuth support | Yes | Yes |
|
||||
| API Keys | N/A | Yes |
|
||||
| LivePhoto/MotionPhoto backup and playback | Yes | Yes |
|
||||
| LivePhoto backup and playback | iOS | Yes |
|
||||
| User-defined storage structure | Yes | Yes |
|
||||
| Public Sharing | No | Yes |
|
||||
| Archive and Favorites | Yes | Yes |
|
||||
|
@ -97,9 +95,8 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
|||
| Memories (x years ago) | Yes | Yes |
|
||||
| Offline support | Yes | No |
|
||||
| Read-only gallery | Yes | Yes |
|
||||
| Stacked Photos | Yes | Yes |
|
||||
|
||||
## Support the project
|
||||
# Support the project
|
||||
|
||||
I've committed to this project, and I will not stop. I will keep updating the docs, adding new features, and fixing bugs. But I can't do it alone. So I need your help to give me additional motivation to keep going.
|
||||
|
||||
|
@ -107,16 +104,10 @@ As our hosts in the [selfhosted.show - In the episode 'The-organization-must-not
|
|||
|
||||
If you feel like this is the right cause and the app is something you are seeing yourself using for a long time, please consider supporting the project with the option below.
|
||||
|
||||
### Donation
|
||||
## Donation
|
||||
|
||||
- [Monthly donation](https://github.com/sponsors/alextran1502) via GitHub Sponsors
|
||||
- [One-time donation](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
|
|
|
@ -19,15 +19,13 @@
|
|||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_ca_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## Avís legal
|
||||
|
@ -111,4 +109,4 @@ Si creieu que aquesta és una causa justa i l'aplicació és alguna cosa que us
|
|||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
||||
|
|
122
README_de_DE.md
|
@ -1,122 +0,0 @@
|
|||
<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>
|
|
@ -19,15 +19,12 @@
|
|||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## Descargo de responsabilidad
|
||||
|
@ -112,4 +109,3 @@ Si consideras que esta es una causa justa y la aplicación es algo que te gustar
|
|||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
|
|
@ -18,16 +18,14 @@
|
|||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## Clause de non-responsabilité
|
||||
|
@ -113,4 +111,3 @@ Si vous estimez que c'est pour la bonne cause et que vous prévoyez d'utiliser l
|
|||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
|
|
@ -19,15 +19,13 @@
|
|||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</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_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
</p>
|
||||
|
||||
## Declino di responsabilità
|
||||
|
@ -113,4 +111,3 @@ Se pensi che Immich sia una buona causa e che l'app sia qualcosa che useresti ne
|
|||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
|
|
@ -18,16 +18,13 @@
|
|||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</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_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## 免責事項
|
||||
|
|
117
README_ko_KR.md
|
@ -1,117 +0,0 @@
|
|||
<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="License: 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 With Custom URL">
|
||||
</p>
|
||||
<h3 align="center">Immich - 고성능 자체 호스팅 사진 및 동영상 백업 솔루션</h3>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
<img src="design/immich-screenshots.png" title="Main 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_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>
|
||||
</p>
|
||||
|
||||
## 주의 사항
|
||||
|
||||
- ⚠️ 이 프로젝트는 **매우 활발히** 개발 중입니다.
|
||||
- ⚠️ 버그 및 잦은 변경 사항이 있을 수 있습니다.
|
||||
- ⚠️ **사진과 동영상을 저장하는 유일한 방법으로 사용하지 마세요.**
|
||||
- ⚠️ 중요한 사진과 동영상을 위해 항상 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 백업 계획을 따르세요!
|
||||
|
||||
## 목차
|
||||
|
||||
- [공식 문서](https://immich.app/docs)
|
||||
- [로드맵](https://github.com/orgs/immich-app/projects/1)
|
||||
- [데모](#demo)
|
||||
- [기능](#features)
|
||||
- [소개](https://immich.app/docs/overview/introduction)
|
||||
- [설치](https://immich.app/docs/install/requirements)
|
||||
- [기여 가이드](https://immich.app/docs/overview/support-the-project)
|
||||
- [프로젝트 지원](#support-the-project)
|
||||
|
||||
## 문서
|
||||
|
||||
설치 가이드를 포함한 주요 문서는 https://immich.app 에서 확인할 수 있습니다.
|
||||
|
||||
## 데모
|
||||
|
||||
https://demo.immich.app 에서 웹 데모를 체험할 수 있습니다.
|
||||
|
||||
모바일 앱의 경우 `서버 엔드포인트 URL`에 `https://demo.immich.app`를 입력합니다.
|
||||
|
||||
```bash title="Demo Credential"
|
||||
자격 증명
|
||||
email: demo@immich.app
|
||||
password: demo
|
||||
```
|
||||
|
||||
```
|
||||
사양: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
```
|
||||
|
||||
## 기능
|
||||
|
||||
| 기능 | 모바일 | 웹 |
|
||||
| ------------------------------------ | ----- | ----- |
|
||||
| 사진, 동영상 업로드 및 보기 | 예 | 예 |
|
||||
| 앱을 열 때 자동으로 백업 | 예 | N/A |
|
||||
| 백업용 앨범 선택 | 예 | N/A |
|
||||
| 로컬 기기로 사진 및 동영상 다운로드 | 예 | 예 |
|
||||
| 다른 사용자 추가 | 예 | 예 |
|
||||
| 앨범 및 공유 앨범 | 예 | 예 |
|
||||
| 스와이프/드래그 가능한 스크롤 바 | 예 | 예 |
|
||||
| RAW 포맷 지원 | 예 | 예 |
|
||||
| 메타데이터 보기 (EXIF, 위치) | 예 | 예 |
|
||||
| 메타데이터, 사물, 얼굴 및 클립으로 검색 | 예 | 예 |
|
||||
| 관리 기능 (사용자 관리) | 아니요 | 예 |
|
||||
| 백그라운드 백업 | 예 | N/A |
|
||||
| 가상 스크롤 | 예 | 예 |
|
||||
| OAuth 지원 | 예 | 예 |
|
||||
| API 키 | N/A | 예 |
|
||||
| 라이브 포토/모션 포토 백업 및 재생 | 예 | 예 |
|
||||
| 사용자 정의 스토리지 구조 | 예 | 예 |
|
||||
| 모든 사용자와 공유 | 아니요 | 예 |
|
||||
| 아카이브 및 즐겨찾기 | 예 |예|
|
||||
| 글로벌 지도 | 예 | 예 |
|
||||
| 특정 사용자와 공유 | 예 | 예 |
|
||||
| 얼굴 인식 및 클러스터링 | 예 | 예 |
|
||||
| 추억 (~년 전) | 예 | 예 |
|
||||
| 오프라인 지원 | 예 | 아니요 |
|
||||
| 읽기 전용 갤러리 | 예 | 예 |
|
||||
| 사진 스택 | 예 | 예 |
|
||||
|
||||
## 프로젝트 지원
|
||||
|
||||
저는 이 프로젝트에 전념해왔고, 앞으로도 멈추지 않을 것입니다. 문서를 업데이트하고, 새로운 기능을 추가하고, 버그를 수정하려 합니다. 하지만 혼자서는 할 수 없습니다. 계속해서 나아갈 수 있는 추가적인 동기부여를 위해 당신의 도움이 필요합니다.
|
||||
|
||||
[selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 진행자가 말했듯이, 우리가 하고 있는 것은 대규모 프로젝트입니다. 언젠가는 이 일을 풀타임으로 하는 것을 희망하며, 이를 실현하기 위해 당신의 도움이 필요합니다.
|
||||
|
||||
만약 이에 동의하거나 이 앱을 장기간 사용하고자 한다면, 아래의 수단을 통해 이 프로젝트를 지원해 주세요.
|
||||
|
||||
### 후원
|
||||
|
||||
- GitHub 스폰서를 통한 [정기 후원](https://github.com/sponsors/alextran1502)
|
||||
- GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
|
@ -18,16 +18,14 @@
|
|||
</a>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_tr_TR.md">Türkçe</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_nl_NL.md">Nederlands</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>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## Disclaimer
|
||||
|
@ -113,4 +111,3 @@ Als je denkt dat dit het juiste doel is en de app iets is dat je jezelf al heel
|
|||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
|
|
@ -19,15 +19,13 @@
|
|||
<br/>
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_zh_CN.md">中文</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_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_zh_CN.md">中文</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
## Feragatname
|
||||
|
@ -110,4 +108,3 @@ Eğer bu size doğru bir amaç gibi geliyorsa ve uygulamanın uzun bir süre boy
|
|||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz
|
||||
|
|
|
@ -23,17 +23,16 @@
|
|||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<a href="README_tr_TR.md">Türkçe</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_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_ja_JP.md">日本語</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
</p>
|
||||
|
||||
|
||||
## 免责声明
|
||||
|
||||
- ⚠️ 本项目正在 **非常活跃** 地开发中。
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
**/*.spec.js
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
.prettierignore
|
||||
.prettierrc
|
||||
package-lock.json
|
||||
testSetup.js
|
||||
tsconfig.json
|
||||
tsconfig.build.json
|
|
@ -1,19 +1,46 @@
|
|||
A command-line interface for interfacing with the self-hosted photo manager [Immich](https://immich.app/).
|
||||
A command-line interface for interfacing with Immich
|
||||
|
||||
Please see the [Immich CLI documentation](https://immich.app/docs/features/command-line-interface).
|
||||
# Getting started
|
||||
|
||||
# For developers
|
||||
$ ts-node cli/src
|
||||
|
||||
To run the Immich CLI from source, run the following in the cli folder:
|
||||
To start using the CLI, you need to login with an API key first:
|
||||
|
||||
$ npm run build
|
||||
$ ts-node .
|
||||
$ ts-node cli/src login-key https://your-immich-instance/api your-api-key
|
||||
|
||||
You'll need ts-node, the easiest way to install it is to use npm:
|
||||
NOTE: This will store your api key under ~/.config/immich/auth.yml
|
||||
|
||||
$ npm i -g ts-node
|
||||
Next, you can run commands:
|
||||
|
||||
You can also build and install the CLI using
|
||||
$ ts-node cli/src server-info
|
||||
|
||||
$ npm run build
|
||||
$ npm install -g .
|
||||
When you're done, log out to remove the credentials from your filesystem
|
||||
|
||||
$ ts-node cli/src logout
|
||||
|
||||
# Usage
|
||||
|
||||
```
|
||||
Usage: immich [options] [command]
|
||||
|
||||
Immich command line interface
|
||||
|
||||
Options:
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
upload [options] [paths...] Upload assets
|
||||
import [options] [paths...] Import existing assets
|
||||
server-info Display server information
|
||||
login-key [instanceUrl] [apiKey] Login using an API key
|
||||
help [command] display help for command
|
||||
```
|
||||
|
||||
# Todo
|
||||
|
||||
- Sidecar should check both .jpg.xmp and .xmp
|
||||
- Sidecar check could be case-insensitive
|
||||
|
||||
# Known issues
|
||||
|
||||
- Upload can't use sdk due to multiple issues
|
||||
|
|
1565
cli/package-lock.json
generated
|
@ -1,23 +1,14 @@
|
|||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.0.4",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"immich": "./dist/src/index.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"immich",
|
||||
"cli"
|
||||
],
|
||||
"name": "immich-cli",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.4.0",
|
||||
"byte-size": "^8.1.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^11.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"picomatch": "^2.3.1",
|
||||
"systeminformation": "^5.18.4",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -29,14 +20,14 @@
|
|||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^49.0.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-extended": "^4.0.0",
|
||||
"jest-message-util": "^29.5.0",
|
||||
|
@ -46,17 +37,15 @@
|
|||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"prepack": "npm run build",
|
||||
"prepack": "yarn build ",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"check": "tsc --noEmit"
|
||||
"format": "prettier --check ."
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
|
@ -73,15 +62,7 @@
|
|||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.(t|j)s"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@api(|/.*)$": "<rootDir>/src/api/$1"
|
||||
},
|
||||
"coverageDirectory": "./coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "github:immich-app/immich",
|
||||
"directory": "cli"
|
||||
}
|
||||
}
|
||||
|
|
3
cli/src/__mocks__/axios.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
// ./__mocks__/axios.js
|
||||
import mockAxios from 'jest-mock-axios';
|
||||
export default mockAxios;
|
|
@ -11,7 +11,6 @@ import {
|
|||
UserApi,
|
||||
} from './open-api';
|
||||
import { ApiConfiguration } from '../cores/api-configuration';
|
||||
import FormData from 'form-data';
|
||||
|
||||
export class ImmichApi {
|
||||
public userApi: UserApi;
|
||||
|
@ -36,7 +35,6 @@ export class ImmichApi {
|
|||
'x-api-key': apiKey,
|
||||
},
|
||||
},
|
||||
formDataCtor: FormData,
|
||||
});
|
||||
|
||||
this.userApi = new UserApi(this.config);
|
||||
|
|
4513
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.89.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* 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.89.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* 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.89.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* 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.89.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
|
|||
export abstract class BaseCommand {
|
||||
protected sessionService!: SessionService;
|
||||
protected immichApi!: ImmichApi;
|
||||
protected deviceId!: string;
|
||||
protected user!: UserResponseDto;
|
||||
protected serverVersion!: ServerVersionResponseDto;
|
||||
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { BaseCommand } from '../cli/base-command';
|
||||
|
||||
export default class ServerInfo extends BaseCommand {
|
||||
static description = 'Display server information';
|
||||
static enableJsonFlag = true;
|
||||
|
||||
public async run() {
|
||||
console.log('Getting server information');
|
||||
|
||||
await this.connect();
|
||||
const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion();
|
||||
|
||||
console.log(`Server is running version ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
||||
|
||||
const { data: supportedmedia } = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
|
||||
|
||||
console.log(`Supported image types: ${supportedmedia.image.map((extension) => extension.replace('.', ''))}`);
|
||||
|
||||
console.log(`Supported video types: ${supportedmedia.video.map((extension) => extension.replace('.', ''))}`);
|
||||
|
||||
const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics();
|
||||
console.log(`Images: ${statistics.images}, Videos: ${statistics.videos}, Total: ${statistics.total}`);
|
||||
console.log(versionInfo);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,43 @@
|
|||
import { Asset } from '../cores/models/asset';
|
||||
import { CrawlService } from '../services';
|
||||
import { BaseCommand } from '../cli/base-command';
|
||||
import { CrawledAsset } from '../cores/models/crawled-asset';
|
||||
import { CrawlService, UploadService } from '../services';
|
||||
import * as si from 'systeminformation';
|
||||
import FormData from 'form-data';
|
||||
import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
|
||||
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
|
||||
|
||||
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 {
|
||||
private crawlService = new CrawlService();
|
||||
private uploadService!: UploadService;
|
||||
deviceId!: string;
|
||||
uploadLength!: number;
|
||||
dryRun = false;
|
||||
|
||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||
await this.connect();
|
||||
|
||||
const deviceId = 'CLI';
|
||||
const uuid = await si.uuid();
|
||||
this.deviceId = uuid.os || 'CLI';
|
||||
this.uploadService = new UploadService(this.immichApi.apiConfiguration);
|
||||
|
||||
const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video);
|
||||
this.dryRun = options.dryRun;
|
||||
|
||||
const crawlOptions = new CrawlOptionsDto();
|
||||
crawlOptions.pathsToCrawl = paths;
|
||||
crawlOptions.recursive = options.recursive;
|
||||
crawlOptions.exclusionPatterns = options.exclusionPatterns;
|
||||
crawlOptions.excludePatterns = options.excludePatterns;
|
||||
|
||||
const crawledFiles: string[] = await crawlService.crawl(crawlOptions);
|
||||
const crawledFiles: string[] = await this.crawlService.crawl(crawlOptions);
|
||||
|
||||
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 CrawledAsset(path));
|
||||
|
||||
const uploadProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
|
@ -53,108 +58,118 @@ export default class Upload extends BaseCommand {
|
|||
totalSize += asset.fileSize;
|
||||
}
|
||||
|
||||
const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data;
|
||||
|
||||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
||||
try {
|
||||
for (const asset of assetsToUpload) {
|
||||
uploadProgress.update({
|
||||
filename: asset.path,
|
||||
});
|
||||
for (const asset of assetsToUpload) {
|
||||
uploadProgress.update({
|
||||
filename: asset.path,
|
||||
});
|
||||
|
||||
let skipUpload = false;
|
||||
if (!options.skipHash) {
|
||||
const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
|
||||
try {
|
||||
if (options.import) {
|
||||
const importData = {
|
||||
assetPath: asset.path,
|
||||
sidecarPath: asset.sidecarPath,
|
||||
deviceAssetId: asset.deviceAssetId,
|
||||
deviceId: this.deviceId,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: false,
|
||||
isReadOnly: options.readOnly,
|
||||
};
|
||||
|
||||
const checkResponse = await this.immichApi.assetApi.checkBulkUpload({
|
||||
assetBulkUploadCheckDto,
|
||||
});
|
||||
|
||||
skipUpload = checkResponse.data.results[0].action === 'reject';
|
||||
}
|
||||
|
||||
if (!skipUpload) {
|
||||
if (!options.dryRun) {
|
||||
const formData = asset.getUploadFormData();
|
||||
const res = await this.uploadAsset(formData);
|
||||
|
||||
if (options.album && asset.albumName) {
|
||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
||||
if (!album) {
|
||||
const res = await this.immichApi.albumApi.createAlbum({
|
||||
createAlbumDto: { albumName: asset.albumName },
|
||||
});
|
||||
album = res.data;
|
||||
existingAlbums.push(album);
|
||||
}
|
||||
|
||||
await this.immichApi.albumApi.addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: [res.data.id] } });
|
||||
}
|
||||
if (!this.dryRun) {
|
||||
await this.uploadService.import(importData);
|
||||
}
|
||||
|
||||
totalSizeUploaded += asset.fileSize;
|
||||
uploadCounter++;
|
||||
} else {
|
||||
await this.uploadAsset(asset, options.skipHash);
|
||||
}
|
||||
|
||||
sizeSoFar += asset.fileSize;
|
||||
|
||||
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
|
||||
} catch (error) {
|
||||
uploadProgress.stop();
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
uploadProgress.stop();
|
||||
|
||||
sizeSoFar += asset.fileSize;
|
||||
if (!asset.skipped) {
|
||||
totalSizeUploaded += asset.fileSize;
|
||||
uploadCounter++;
|
||||
}
|
||||
|
||||
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
|
||||
}
|
||||
|
||||
uploadProgress.stop();
|
||||
|
||||
let messageStart;
|
||||
if (options.dryRun) {
|
||||
messageStart = 'Would have';
|
||||
if (this.dryRun) {
|
||||
messageStart = 'Would have ';
|
||||
} else {
|
||||
messageStart = 'Successfully';
|
||||
messageStart = 'Successfully ';
|
||||
}
|
||||
|
||||
if (uploadCounter === 0) {
|
||||
console.log('All assets were already uploaded, nothing to do.');
|
||||
if (options.import) {
|
||||
console.log(`${messageStart} imported ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
|
||||
} else {
|
||||
console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
|
||||
}
|
||||
if (options.delete) {
|
||||
if (options.dryRun) {
|
||||
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
||||
if (uploadCounter === 0) {
|
||||
console.log('All assets were already uploaded, nothing to do.');
|
||||
} else {
|
||||
console.log('Deleting assets that have been uploaded...');
|
||||
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
|
||||
deletionProgress.start(crawledFiles.length, 0);
|
||||
console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
|
||||
}
|
||||
if (options.delete) {
|
||||
if (this.dryRun) {
|
||||
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
||||
} else {
|
||||
console.log('Deleting assets that have been uploaded...');
|
||||
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
|
||||
deletionProgress.start(crawledFiles.length, 0);
|
||||
|
||||
for (const asset of assetsToUpload) {
|
||||
if (!options.dryRun) {
|
||||
await asset.delete();
|
||||
for (const asset of assetsToUpload) {
|
||||
if (!this.dryRun) {
|
||||
await asset.delete();
|
||||
}
|
||||
deletionProgress.increment();
|
||||
}
|
||||
deletionProgress.increment();
|
||||
deletionProgress.stop();
|
||||
console.log('Deletion complete');
|
||||
}
|
||||
deletionProgress.stop();
|
||||
console.log('Deletion complete');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadAsset(data: FormData): Promise<axios.AxiosResponse> {
|
||||
const url = this.immichApi.apiConfiguration.instanceUrl + '/asset/upload';
|
||||
private async uploadAsset(asset: CrawledAsset, skipHash = false) {
|
||||
await asset.readData();
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method: 'post',
|
||||
maxRedirects: 0,
|
||||
url,
|
||||
headers: {
|
||||
'x-api-key': this.immichApi.apiConfiguration.apiKey,
|
||||
...data.getHeaders(),
|
||||
},
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
data,
|
||||
};
|
||||
let skipUpload = false;
|
||||
if (!skipHash) {
|
||||
const checksum = await asset.hash();
|
||||
|
||||
const res = await axios(config);
|
||||
return res;
|
||||
const checkResponse = await this.uploadService.checkIfAssetAlreadyExists(asset.path, checksum);
|
||||
skipUpload = checkResponse.data.results[0].action === 'reject';
|
||||
}
|
||||
|
||||
if (skipUpload) {
|
||||
asset.skipped = true;
|
||||
} else {
|
||||
const uploadFormData = new FormData();
|
||||
|
||||
uploadFormData.append('deviceAssetId', asset.deviceAssetId);
|
||||
uploadFormData.append('deviceId', this.deviceId);
|
||||
uploadFormData.append('fileCreatedAt', asset.fileCreatedAt);
|
||||
uploadFormData.append('fileModifiedAt', asset.fileModifiedAt);
|
||||
uploadFormData.append('isFavorite', String(false));
|
||||
uploadFormData.append('assetData', asset.assetData, { filename: asset.path });
|
||||
|
||||
if (asset.sidecarData) {
|
||||
uploadFormData.append('sidecarData', asset.sidecarData, {
|
||||
filename: asset.sidecarPath,
|
||||
contentType: 'application/xml',
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.dryRun) {
|
||||
await this.uploadService.upload(uploadFormData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
59
cli/src/cores/constants.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Check asset-upload.config.spec.ts for complete list
|
||||
// TODO: we should get this list from the server via API in the future
|
||||
|
||||
// Videos
|
||||
const videos = ['mp4', 'webm', 'mov', '3gp', 'avi', 'm2ts', 'mts', 'mpg', 'flv', 'mkv', 'wmv'];
|
||||
|
||||
// Images
|
||||
const heic = ['heic', 'heif'];
|
||||
const jpeg = ['jpg', 'jpeg'];
|
||||
const png = ['png'];
|
||||
const gif = ['gif'];
|
||||
const tiff = ['tif', 'tiff'];
|
||||
const webp = ['webp'];
|
||||
const dng = ['dng'];
|
||||
const other = [
|
||||
'3fr',
|
||||
'ari',
|
||||
'arw',
|
||||
'avif',
|
||||
'cap',
|
||||
'cin',
|
||||
'cr2',
|
||||
'cr3',
|
||||
'crw',
|
||||
'dcr',
|
||||
'nef',
|
||||
'erf',
|
||||
'fff',
|
||||
'iiq',
|
||||
'jxl',
|
||||
'k25',
|
||||
'kdc',
|
||||
'mrw',
|
||||
'orf',
|
||||
'ori',
|
||||
'pef',
|
||||
'psd',
|
||||
'raf',
|
||||
'raw',
|
||||
'rwl',
|
||||
'sr2',
|
||||
'srf',
|
||||
'srw',
|
||||
'orf',
|
||||
'ori',
|
||||
'x3f',
|
||||
];
|
||||
|
||||
export const ACCEPTED_FILE_EXTENSIONS = [
|
||||
...videos,
|
||||
...jpeg,
|
||||
...png,
|
||||
...heic,
|
||||
...gif,
|
||||
...tiff,
|
||||
...webp,
|
||||
...dng,
|
||||
...other,
|
||||
];
|
|
@ -1,6 +1,6 @@
|
|||
export class CrawlOptionsDto {
|
||||
pathsToCrawl!: string[];
|
||||
recursive? = false;
|
||||
includeHidden? = false;
|
||||
exclusionPatterns?: string[];
|
||||
recursive = false;
|
||||
includeHidden = false;
|
||||
excludePatterns!: string[];
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
export class UploadOptionsDto {
|
||||
recursive = false;
|
||||
exclusionPatterns!: string[];
|
||||
excludePatterns!: string[];
|
||||
dryRun = false;
|
||||
skipHash = false;
|
||||
delete = false;
|
||||
import = false;
|
||||
readOnly = true;
|
||||
album = false;
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './constants';
|
||||
export * from './models';
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
import * as fs from 'fs';
|
||||
import { basename } from 'node:path';
|
||||
import crypto from 'crypto';
|
||||
import Os from 'os';
|
||||
import FormData from 'form-data';
|
||||
|
||||
export class Asset {
|
||||
readonly path: string;
|
||||
readonly deviceId!: string;
|
||||
|
||||
assetData?: fs.ReadStream;
|
||||
deviceAssetId?: string;
|
||||
fileCreatedAt?: string;
|
||||
fileModifiedAt?: string;
|
||||
sidecarData?: fs.ReadStream;
|
||||
sidecarPath?: string;
|
||||
fileSize!: number;
|
||||
albumName?: string;
|
||||
|
||||
constructor(path: string, deviceId: string) {
|
||||
this.path = path;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
async process() {
|
||||
const stats = await fs.promises.stat(this.path);
|
||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
|
||||
this.fileCreatedAt = stats.mtime.toISOString();
|
||||
this.fileModifiedAt = stats.mtime.toISOString();
|
||||
this.fileSize = stats.size;
|
||||
this.albumName = this.extractAlbumName();
|
||||
|
||||
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 = this.getReadStream(sideCarPath);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
const data: any = {
|
||||
assetData: this.assetData as any,
|
||||
deviceAssetId: this.deviceAssetId,
|
||||
deviceId: this.deviceId,
|
||||
fileCreatedAt: this.fileCreatedAt,
|
||||
fileModifiedAt: this.fileModifiedAt,
|
||||
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 getReadStream(path: string): fs.ReadStream {
|
||||
return fs.createReadStream(path);
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
return fs.promises.unlink(this.path);
|
||||
}
|
||||
|
||||
public async hash(): Promise<string> {
|
||||
const sha1 = (filePath: string) => {
|
||||
const hash = crypto.createHash('sha1');
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const rs = fs.createReadStream(filePath);
|
||||
rs.on('error', reject);
|
||||
rs.on('data', (chunk) => hash.update(chunk));
|
||||
rs.on('end', () => resolve(hash.digest('hex')));
|
||||
});
|
||||
};
|
||||
|
||||
return await sha1(this.path);
|
||||
}
|
||||
|
||||
private extractAlbumName(): string {
|
||||
if (Os.platform() === 'win32') {
|
||||
return this.path.split('\\').slice(-2)[0];
|
||||
} else {
|
||||
return this.path.split('/').slice(-2)[0];
|
||||
}
|
||||
}
|
||||
}
|
58
cli/src/cores/models/crawled-asset.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import * as fs from 'fs';
|
||||
import { basename } from 'node:path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export class CrawledAsset {
|
||||
public path: string;
|
||||
|
||||
public assetData?: fs.ReadStream;
|
||||
public deviceAssetId?: string;
|
||||
public fileCreatedAt?: string;
|
||||
public fileModifiedAt?: string;
|
||||
public sidecarData?: Buffer;
|
||||
public sidecarPath?: string;
|
||||
public fileSize!: number;
|
||||
public skipped = false;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async readData() {
|
||||
this.assetData = fs.createReadStream(this.path);
|
||||
}
|
||||
|
||||
async process() {
|
||||
const stats = await fs.promises.stat(this.path);
|
||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
|
||||
this.fileCreatedAt = stats.mtime.toISOString();
|
||||
this.fileModifiedAt = stats.mtime.toISOString();
|
||||
this.fileSize = stats.size;
|
||||
|
||||
// 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 fs.promises.readFile(sideCarPath);
|
||||
this.sidecarPath = sideCarPath;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
return fs.promises.unlink(this.path);
|
||||
}
|
||||
|
||||
public async hash(): Promise<string> {
|
||||
const sha1 = (filePath: string) => {
|
||||
const hash = crypto.createHash('sha1');
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const rs = fs.createReadStream(filePath);
|
||||
rs.on('error', reject);
|
||||
rs.on('data', (chunk) => hash.update(chunk));
|
||||
rs.on('end', () => resolve(hash.digest('hex')));
|
||||
});
|
||||
};
|
||||
|
||||
return await sha1(this.path);
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
export * from './asset';
|
||||
export * from './crawled-asset';
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
#! /usr/bin/env node
|
||||
|
||||
import { program, Option } 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').version(version);
|
||||
program.name('immich').description('Immich command line interface');
|
||||
|
||||
program
|
||||
.command('upload')
|
||||
|
@ -16,11 +12,6 @@ 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('-a, --album', 'Automatically create albums based on folder name')
|
||||
.env('IMMICH_AUTO_CREATE_ALBUM')
|
||||
.default(false),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
|
||||
.env('IMMICH_DRY_RUN')
|
||||
|
@ -29,13 +20,33 @@ program
|
|||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||
.action(async (paths, options) => {
|
||||
options.exclusionPatterns = options.ignore;
|
||||
options.excludePatterns = options.ignore;
|
||||
await new Upload().run(paths, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('import')
|
||||
.description('Import existing assets')
|
||||
.usage('[options] [paths...]')
|
||||
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
|
||||
.addOption(
|
||||
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
|
||||
.env('IMMICH_DRY_RUN')
|
||||
.default(false),
|
||||
)
|
||||
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
|
||||
.addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them'))
|
||||
.argument('[paths...]', 'One or more paths to assets to be imported')
|
||||
.action(async (paths, options) => {
|
||||
options.import = true;
|
||||
options.excludePatterns = options.ignore;
|
||||
await new Upload().run(paths, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('server-info')
|
||||
.description('Display server information')
|
||||
|
||||
.action(async () => {
|
||||
await new ServerInfo().run();
|
||||
});
|
||||
|
@ -49,11 +60,4 @@ program
|
|||
await new LoginKey().run(paths, options);
|
||||
});
|
||||
|
||||
program
|
||||
.command('logout')
|
||||
.description('Remove stored credentials')
|
||||
.action(async () => {
|
||||
await new Logout().run();
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
|
|
@ -1,206 +1,235 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { CrawlService } from './crawl.service';
|
||||
import mockfs from 'mock-fs';
|
||||
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
|
||||
import { CrawlService } from '.';
|
||||
import { toIncludeSameMembers } from 'jest-extended';
|
||||
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
|
||||
|
||||
interface Test {
|
||||
test: string;
|
||||
options: CrawlOptionsDto;
|
||||
files: Record<string, boolean>;
|
||||
}
|
||||
const matchers = require('jest-extended');
|
||||
expect.extend(matchers);
|
||||
|
||||
const cwd = process.cwd();
|
||||
const crawlService = new CrawlService();
|
||||
|
||||
const tests: Test[] = [
|
||||
{
|
||||
test: 'should return empty when crawling an empty path list',
|
||||
options: {
|
||||
pathsToCrawl: [],
|
||||
},
|
||||
files: {},
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single path',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should exclude by file extension',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
exclusionPatterns: ['**/*.tif'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/image.tif': false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should exclude by file extension without case sensitivity',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
exclusionPatterns: ['**/*.TIF'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/image.tif': false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should exclude by folder',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
exclusionPatterns: ['**/raw/**'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/raw/image.jpg': false,
|
||||
'/photos/raw2/image.jpg': true,
|
||||
'/photos/folder/raw/image.jpg': false,
|
||||
'/photos/crawl/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should crawl multiple paths',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/', '/images/', '/albums/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image1.jpg': true,
|
||||
'/images/image2.jpg': true,
|
||||
'/albums/image3.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should support globbing paths',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos*'],
|
||||
},
|
||||
files: {
|
||||
'/photos1/image1.jpg': true,
|
||||
'/photos2/image2.jpg': true,
|
||||
'/images/image3.jpg': false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single path without trailing slash',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single path',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/subfolder/image1.jpg': true,
|
||||
'/photos/subfolder/image2.jpg': true,
|
||||
'/image1.jpg': false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should filter file extensions',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/image.txt': false,
|
||||
'/photos/1': false,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should include photo and video extensions',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/', '/videos/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/image.jpeg': true,
|
||||
'/photos/image.heic': true,
|
||||
'/photos/image.heif': true,
|
||||
'/photos/image.png': true,
|
||||
'/photos/image.gif': true,
|
||||
'/photos/image.tif': true,
|
||||
'/photos/image.tiff': true,
|
||||
'/photos/image.webp': true,
|
||||
'/photos/image.dng': true,
|
||||
'/photos/image.nef': true,
|
||||
'/videos/video.mp4': true,
|
||||
'/videos/video.mov': true,
|
||||
'/videos/video.webm': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should check file extensions without case sensitivity',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/'],
|
||||
},
|
||||
files: {
|
||||
'/photos/image.jpg': true,
|
||||
'/photos/image.Jpg': true,
|
||||
'/photos/image.jpG': true,
|
||||
'/photos/image.JPG': true,
|
||||
'/photos/image.jpEg': true,
|
||||
'/photos/image.TIFF': true,
|
||||
'/photos/image.tif': true,
|
||||
'/photos/image.dng': true,
|
||||
'/photos/image.NEF': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should normalize the path',
|
||||
options: {
|
||||
pathsToCrawl: ['/photos/1/../2'],
|
||||
},
|
||||
files: {
|
||||
'/photos/1/image.jpg': false,
|
||||
'/photos/2/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should return absolute paths',
|
||||
options: {
|
||||
pathsToCrawl: ['photos'],
|
||||
},
|
||||
files: {
|
||||
[`${cwd}/photos/1.jpg`]: true,
|
||||
[`${cwd}/photos/2.jpg`]: true,
|
||||
[`/photos/3.jpg`]: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
describe('CrawlService', () => {
|
||||
beforeAll(() => {
|
||||
// Write a dummy output before mock-fs to prevent some annoying errors
|
||||
console.log();
|
||||
});
|
||||
|
||||
describe(CrawlService.name, () => {
|
||||
const sut = new CrawlService(
|
||||
['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'],
|
||||
['.mov', '.mp4', '.webm'],
|
||||
);
|
||||
it('should crawl a single directory', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should crawl a single file', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/image.jpg'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should crawl a file and a directory', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/images/photo.jpg': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/image.jpg', '/images/'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/images/photo.jpg']);
|
||||
});
|
||||
|
||||
it('should exclude by file extension', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/image.tif': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
options.excludePatterns = ['**/*.tif'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should exclude by file extension without case sensitivity', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/image.tif': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
options.excludePatterns = ['**/*.TIF'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should exclude by folder', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/raw/image.jpg': '',
|
||||
'/photos/raw2/image.jpg': '',
|
||||
'/photos/folder/raw/image.jpg': '',
|
||||
'/photos/crawl/image.jpg': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
options.excludePatterns = ['**/raw/**'];
|
||||
options.recursive = true;
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/photos/raw2/image.jpg', '/photos/crawl/image.jpg']);
|
||||
});
|
||||
|
||||
it('should crawl multiple paths', async () => {
|
||||
mockfs({
|
||||
'/photos/image1.jpg': '',
|
||||
'/images/image2.jpg': '',
|
||||
'/albums/image3.jpg': '',
|
||||
});
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/', '/images/', '/albums/'];
|
||||
options.recursive = false;
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image1.jpg', '/images/image2.jpg', '/albums/image3.jpg']);
|
||||
});
|
||||
|
||||
it('should crawl a single path without trailing slash', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
});
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should crawl a single path without recursion', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/subfolder/image1.jpg': '',
|
||||
'/photos/subfolder/image2.jpg': '',
|
||||
'/image1.jpg': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should crawl a single path with recursion', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/subfolder/image1.jpg': '',
|
||||
'/photos/subfolder/image2.jpg': '',
|
||||
'/image1.jpg': '',
|
||||
});
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
options.recursive = true;
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers([
|
||||
'/photos/image.jpg',
|
||||
'/photos/subfolder/image1.jpg',
|
||||
'/photos/subfolder/image2.jpg',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter file extensions', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/image.txt': '',
|
||||
'/photos/1': '',
|
||||
});
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||
});
|
||||
|
||||
it('should include photo and video extensions', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/image.jpeg': '',
|
||||
'/photos/image.heic': '',
|
||||
'/photos/image.heif': '',
|
||||
'/photos/image.png': '',
|
||||
'/photos/image.gif': '',
|
||||
'/photos/image.tif': '',
|
||||
'/photos/image.tiff': '',
|
||||
'/photos/image.webp': '',
|
||||
'/photos/image.dng': '',
|
||||
'/photos/image.nef': '',
|
||||
'/videos/video.mp4': '',
|
||||
'/videos/video.mov': '',
|
||||
'/videos/video.webm': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/', '/videos/'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
|
||||
expect(paths).toIncludeSameMembers([
|
||||
'/photos/image.jpg',
|
||||
'/photos/image.jpeg',
|
||||
'/photos/image.heic',
|
||||
'/photos/image.heif',
|
||||
'/photos/image.png',
|
||||
'/photos/image.gif',
|
||||
'/photos/image.tif',
|
||||
'/photos/image.tiff',
|
||||
'/photos/image.webp',
|
||||
'/photos/image.dng',
|
||||
'/photos/image.nef',
|
||||
'/videos/video.mp4',
|
||||
'/videos/video.mov',
|
||||
'/videos/video.webm',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should check file extensions without case sensitivity', async () => {
|
||||
mockfs({
|
||||
'/photos/image.jpg': '',
|
||||
'/photos/image.Jpg': '',
|
||||
'/photos/image.jpG': '',
|
||||
'/photos/image.JPG': '',
|
||||
'/photos/image.jpEg': '',
|
||||
'/photos/image.TIFF': '',
|
||||
'/photos/image.tif': '',
|
||||
'/photos/image.dng': '',
|
||||
'/photos/image.NEF': '',
|
||||
});
|
||||
|
||||
const options = new CrawlOptionsDto();
|
||||
options.pathsToCrawl = ['/photos/'];
|
||||
const paths: string[] = await crawlService.crawl(options);
|
||||
expect(paths).toIncludeSameMembers([
|
||||
'/photos/image.jpg',
|
||||
'/photos/image.Jpg',
|
||||
'/photos/image.jpG',
|
||||
'/photos/image.JPG',
|
||||
'/photos/image.jpEg',
|
||||
'/photos/image.TIFF',
|
||||
'/photos/image.tif',
|
||||
'/photos/image.dng',
|
||||
'/photos/image.NEF',
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockfs.restore();
|
||||
});
|
||||
|
||||
describe('crawl', () => {
|
||||
for (const { test, options, files } of tests) {
|
||||
it(test, async () => {
|
||||
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
|
||||
|
||||
const actual = await sut.crawl(options);
|
||||
const expected = Object.entries(files)
|
||||
.filter((entry) => entry[1])
|
||||
.map(([file]) => file);
|
||||
|
||||
expect(actual.sort()).toEqual(expected.sort());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,28 +1,47 @@
|
|||
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
|
||||
import { ACCEPTED_FILE_EXTENSIONS } from '../cores';
|
||||
import { glob } from 'glob';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export class CrawlService {
|
||||
private readonly extensions!: string[];
|
||||
public async crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
|
||||
const pathsToCrawl: string[] = crawlOptions.pathsToCrawl;
|
||||
|
||||
constructor(image: string[], video: string[]) {
|
||||
this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
|
||||
}
|
||||
const directories: string[] = [];
|
||||
const crawledFiles: string[] = [];
|
||||
|
||||
crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
|
||||
const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions;
|
||||
if (!pathsToCrawl) {
|
||||
return Promise.resolve([]);
|
||||
for await (const currentPath of pathsToCrawl) {
|
||||
const stats = await fs.promises.stat(currentPath);
|
||||
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||
crawledFiles.push(currentPath);
|
||||
} else {
|
||||
directories.push(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
const base = pathsToCrawl.length === 1 ? pathsToCrawl[0] : `{${pathsToCrawl.join(',')}}`;
|
||||
const extensions = `*{${this.extensions}}`;
|
||||
let searchPattern: string;
|
||||
if (directories.length === 1) {
|
||||
searchPattern = directories[0];
|
||||
} else if (directories.length === 0) {
|
||||
return crawledFiles;
|
||||
} else {
|
||||
searchPattern = '{' + directories.join(',') + '}';
|
||||
}
|
||||
|
||||
return glob(`${base}/**/${extensions}`, {
|
||||
absolute: true,
|
||||
if (crawlOptions.recursive) {
|
||||
searchPattern = searchPattern + '/**/';
|
||||
}
|
||||
|
||||
searchPattern = `${searchPattern}/*.{${ACCEPTED_FILE_EXTENSIONS.join(',')}}`;
|
||||
|
||||
const globbedFiles = await glob(searchPattern, {
|
||||
nocase: true,
|
||||
nodir: true,
|
||||
dot: includeHidden,
|
||||
ignore: exclusionPatterns,
|
||||
ignore: crawlOptions.excludePatterns,
|
||||
});
|
||||
|
||||
const returnedFiles = crawledFiles.concat(globbedFiles);
|
||||
returnedFiles.sort();
|
||||
return returnedFiles;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './upload.service';
|
||||
export * from './crawl.service';
|
||||
|
|
|
@ -46,7 +46,7 @@ export class SessionService {
|
|||
|
||||
// Check if server and api key are valid
|
||||
const { data: userInfo } = await this.api.userApi.getMyUserInfo().catch((error) => {
|
||||
throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
|
||||
throw new LoginError(`Failed to connect to the server: ${error.message}`);
|
||||
});
|
||||
|
||||
console.log(`Logged in as ${userInfo.email}`);
|
||||
|
@ -78,7 +78,7 @@ export class SessionService {
|
|||
|
||||
private async ping(): Promise<void> {
|
||||
const { data: pingResponse } = await this.api.serverInfoApi.pingServer().catch((error) => {
|
||||
throw new Error(`Failed to connect to server ${this.api.apiConfiguration.instanceUrl}: ${error.message}`);
|
||||
throw new Error(`Failed to connect to the server: ${error.message}`);
|
||||
});
|
||||
|
||||
if (pingResponse.res !== 'pong') {
|
||||
|
|
24
cli/src/services/upload.service.spec.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { UploadService } from './upload.service';
|
||||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { ApiConfiguration } from '../cores/api-configuration';
|
||||
|
||||
jest.mock('axios', () => jest.fn());
|
||||
|
||||
describe('UploadService', () => {
|
||||
let uploadService: UploadService;
|
||||
|
||||
beforeEach(() => {
|
||||
const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key');
|
||||
|
||||
uploadService = new UploadService(apiConfiguration);
|
||||
});
|
||||
|
||||
it('should call axios', async () => {
|
||||
const data = new FormData();
|
||||
|
||||
await uploadService.upload(data);
|
||||
|
||||
expect(axios).toHaveBeenCalled();
|
||||
});
|
||||
});
|
65
cli/src/services/upload.service.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { ApiConfiguration } from '../cores/api-configuration';
|
||||
|
||||
export class UploadService {
|
||||
private readonly uploadConfig: AxiosRequestConfig<any>;
|
||||
private readonly checkAssetExistenceConfig: AxiosRequestConfig<any>;
|
||||
private readonly importConfig: AxiosRequestConfig<any>;
|
||||
|
||||
constructor(apiConfiguration: ApiConfiguration) {
|
||||
this.uploadConfig = {
|
||||
method: 'post',
|
||||
maxRedirects: 0,
|
||||
url: `${apiConfiguration.instanceUrl}/asset/upload`,
|
||||
headers: {
|
||||
'x-api-key': apiConfiguration.apiKey,
|
||||
},
|
||||
maxContentLength: Number.POSITIVE_INFINITY,
|
||||
maxBodyLength: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
|
||||
this.importConfig = {
|
||||
method: 'post',
|
||||
maxRedirects: 0,
|
||||
url: `${apiConfiguration.instanceUrl}/asset/import`,
|
||||
headers: {
|
||||
'x-api-key': apiConfiguration.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
maxContentLength: Number.POSITIVE_INFINITY,
|
||||
maxBodyLength: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
|
||||
this.checkAssetExistenceConfig = {
|
||||
method: 'post',
|
||||
maxRedirects: 0,
|
||||
url: `${apiConfiguration.instanceUrl}/asset/bulk-upload-check`,
|
||||
headers: {
|
||||
'x-api-key': apiConfiguration.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public checkIfAssetAlreadyExists(path: string, checksum: string) {
|
||||
this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] });
|
||||
|
||||
// TODO: retry on 500 errors?
|
||||
return axios(this.checkAssetExistenceConfig);
|
||||
}
|
||||
|
||||
public upload(data: FormData) {
|
||||
this.uploadConfig.data = data;
|
||||
|
||||
// TODO: retry on 500 errors?
|
||||
return axios(this.uploadConfig);
|
||||
}
|
||||
|
||||
public import(data: any) {
|
||||
this.importConfig.data = data;
|
||||
|
||||
// TODO: retry on 500 errors?
|
||||
return axios(this.importConfig);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
|
@ -8,7 +8,7 @@
|
|||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"target": "es2017",
|
||||
"moduleResolution": "node16",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
> [!CAUTION]
|
||||
> Make sure to use the docker-compose.yml of the current release:
|
||||
> https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
>
|
||||
> The compose file on main may not be compatible with the latest release.
|
|
@ -1,39 +1,30 @@
|
|||
# See:
|
||||
# - https://immich.app/docs/developer/setup
|
||||
# - https://immich.app/docs/developer/troubleshooting
|
||||
|
||||
version: "3.8"
|
||||
|
||||
name: immich-dev
|
||||
|
||||
x-server-build: &server-common
|
||||
image: immich-server-dev:latest
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
target: dev
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||
- /usr/src/app/node_modules
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: immich-server-dev:latest
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
target: builder
|
||||
command: npm run start:debug immich
|
||||
<<: *server-common
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||
- /usr/src/app/node_modules
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- 3001:3001
|
||||
- 9230:9230
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
|
@ -41,13 +32,30 @@ services:
|
|||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: npm run start:debug microservices
|
||||
<<: *server-common
|
||||
image: immich-microservices:latest
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
target: builder
|
||||
command: npm run start:debug microservices
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||
- /usr/src/app/node_modules
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- 9231:9230
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
depends_on:
|
||||
- database
|
||||
- immich-server
|
||||
|
@ -59,11 +67,16 @@ services:
|
|||
build:
|
||||
context: ../web
|
||||
dockerfile: Dockerfile
|
||||
command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000"
|
||||
target: dev
|
||||
command: npm run dev --host
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# Rename these values for svelte public interface
|
||||
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
|
||||
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
|
||||
ports:
|
||||
- 2283:3000
|
||||
- 3000:3000
|
||||
- 24678:24678
|
||||
volumes:
|
||||
- ../web:/usr/src/app
|
||||
|
@ -108,11 +121,11 @@ services:
|
|||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
|
||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
@ -124,5 +137,22 @@ services:
|
|||
ports:
|
||||
- 5432:5432
|
||||
|
||||
immich-proxy:
|
||||
container_name: immich_proxy
|
||||
image: immich-proxy-dev:latest
|
||||
environment:
|
||||
# Make sure these values get passed through from the env file
|
||||
- IMMICH_SERVER_URL
|
||||
- IMMICH_WEB_URL
|
||||
build:
|
||||
context: ../nginx
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 2283:8080
|
||||
depends_on:
|
||||
- immich-server
|
||||
- immich-web
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
|
|
|
@ -1,44 +1,23 @@
|
|||
version: "3.8"
|
||||
|
||||
name: immich-prod
|
||||
|
||||
x-server-build: &server-common
|
||||
image: immich-server:latest
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
command: [ "./start-server.sh" ]
|
||||
<<: *server-common
|
||||
ports:
|
||||
- 2283:3001
|
||||
image: immich-server:latest
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
command: ["./start-server.sh"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
command: [ "./start-microservices.sh" ]
|
||||
<<: *server-common
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
- immich-server
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
image: immich-machine-learning:latest
|
||||
|
@ -46,11 +25,45 @@ services:
|
|||
context: ../machine-learning
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: immich-microservices:latest
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
command: ["./start-microservices.sh"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- database
|
||||
- immich-server
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-web:
|
||||
container_name: immich_web
|
||||
image: immich-web:latest
|
||||
build:
|
||||
context: ../web
|
||||
dockerfile: Dockerfile
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
depends_on:
|
||||
- immich-server
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
|
@ -60,17 +73,17 @@ services:
|
|||
# remove this to get debug messages
|
||||
- GLOG_minloglevel=1
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/typesense:/data
|
||||
- tsdata:/data
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
|
||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
@ -78,8 +91,28 @@ services:
|
|||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
immich-proxy:
|
||||
container_name: immich_proxy
|
||||
image: immich-proxy:latest
|
||||
environment:
|
||||
# Make sure these values get passed through from the env file
|
||||
- IMMICH_SERVER_URL
|
||||
- IMMICH_WEB_URL
|
||||
build:
|
||||
context: ../nginx
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 2283:8080
|
||||
logging:
|
||||
driver: none
|
||||
depends_on:
|
||||
- immich-server
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
model-cache:
|
||||
tsdata:
|
||||
|
|
|
@ -6,9 +6,9 @@ services:
|
|||
immich-server:
|
||||
image: immich-server-dev:latest
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
target: dev
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
target: builder
|
||||
command: npm run test:e2e
|
||||
volumes:
|
||||
- ../server:/usr/src/app
|
||||
|
@ -23,7 +23,7 @@ services:
|
|||
- database
|
||||
|
||||
database:
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
command: -c fsync=off
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
|
|
@ -1,27 +1,15 @@
|
|||
version: "3.8"
|
||||
|
||||
#
|
||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
#
|
||||
|
||||
name: immich
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
command: ["start.sh", "immich"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- 2283:3001
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
|
@ -34,7 +22,7 @@ services:
|
|||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
command: [ "start.sh", "microservices" ]
|
||||
command: ["start.sh", "microservices"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
@ -55,6 +43,13 @@ services:
|
|||
- .env
|
||||
restart: always
|
||||
|
||||
immich-web:
|
||||
container_name: immich_web
|
||||
image: ghcr.io/immich-app/immich-web:${IMMICH_VERSION:-release}
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
typesense:
|
||||
container_name: immich_typesense
|
||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||
|
@ -69,12 +64,12 @@ services:
|
|||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46
|
||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||
restart: always
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
@ -85,6 +80,20 @@ services:
|
|||
- pgdata:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
immich-proxy:
|
||||
container_name: immich_proxy
|
||||
image: ghcr.io/immich-app/immich-proxy:${IMMICH_VERSION:-release}
|
||||
environment:
|
||||
# Make sure these values get passed through from the env file
|
||||
- IMMICH_SERVER_URL
|
||||
- IMMICH_WEB_URL
|
||||
ports:
|
||||
- 2283:8080
|
||||
depends_on:
|
||||
- immich-server
|
||||
- immich-web
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
model-cache:
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
version: "3.8"
|
||||
|
||||
# Hardware acceleration for transcoding using RKMPP for Rockchip SOCs
|
||||
# This is only needed if you want to use hardware acceleration for transcoding.
|
||||
# Supported host OS is Ubuntu Jammy 22.04 with custom ffmpeg from ppa:liujianfeng1994/rockchip-multimedia
|
||||
|
||||
services:
|
||||
hwaccel:
|
||||
security_opt: # enables full access to /sys and /proc, still far better than privileged: true
|
||||
- systempaths=unconfined
|
||||
- apparmor=unconfined
|
||||
group_add:
|
||||
- video
|
||||
devices:
|
||||
- /dev/rga:/dev/rga
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dma_heap:/dev/dma_heap
|
||||
- /dev/mpp_service:/dev/mpp_service
|
||||
volumes:
|
||||
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
||||
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
||||
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
|
@ -33,6 +33,8 @@ To be concise, Immich can now read in the gallery files, register the path into
|
|||
- Only new files that are added to the gallery will be detected.
|
||||
- Deleted and moved files will not be detected.
|
||||
|
||||
You can find more information on how to use the feature by reading the documentation [here](/docs/features/read-only-gallery).
|
||||
|
||||
## Memory feature
|
||||
|
||||
This is considered a fun feature that the team and I wanted to build for so long, but we had to put it off because of the refactoring of the code base. The code base is now in a good enough form to circle back and add more exciting features.
|
||||
|
|
|
@ -12,9 +12,9 @@ sidebar_position: 7
|
|||
| ![cloud-cross](/img/cloud-off.svg) | Asset is only available locally and has not yet been backed up |
|
||||
| ![cloud-done](/img/cloud-done.svg) | Asset was uploaded from this device and is now backed up in the cloud/server and still available in original on the device |
|
||||
|
||||
### Can I add my existing photo library?
|
||||
### How can I sync an existing directory with Immich's server?
|
||||
|
||||
Yes, with an [external library](/docs/features/libraries.md).
|
||||
Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich.
|
||||
|
||||
### Why are only photos and not videos being uploaded to Immich?
|
||||
|
||||
|
|
|
@ -17,13 +17,13 @@ docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/back
|
|||
```
|
||||
|
||||
```bash title='Restore'
|
||||
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch.
|
||||
docker compose pull # Update to latest version of Immich (if desired)
|
||||
docker compose create # Create Docker containers for Immich apps without running them.
|
||||
docker-compose down -v # CAUTION! Deletes all Immich data to start from scratch.
|
||||
docker-compose pull # Update to latest version of Immich (if desired)
|
||||
docker-compose create # Create Docker containers for Immich apps without running them.
|
||||
docker start immich_postgres # Start Postgres server
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql -U postgres -d immich # Restore Backup
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
docker-compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
||||
Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.).
|
||||
|
|
Before Width: | Height: | Size: 501 KiB After Width: | Height: | Size: 111 KiB |
|
@ -1,6 +1,21 @@
|
|||
# Reverse Proxy
|
||||
|
||||
Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
||||
When deploying Immich it is important to understand that a reverse proxy is required in front of the server and web container. The reverse proxy acts as an intermediary between the user and container, forwarding requests to the correct container based on the URL path.
|
||||
|
||||
## Default Reverse Proxy
|
||||
|
||||
Immich provides a default nginx reverse proxy preconfigured to perform the correct routing and set the necessary headers for the server and web container to use. These headers are crucial to redirect to the correct URL and determine the client's IP address.
|
||||
|
||||
## Using a Different Reverse Proxy
|
||||
|
||||
While the reverse proxy provided by Immich works well for basic deployments, some users may want to use a different reverse proxy. Fortunately, Immich is flexible enough to accommodate different reverse proxies. Users can either:
|
||||
|
||||
1. Add another reverse proxy on top of Immich's reverse proxy
|
||||
2. Completely replace the default reverse proxy
|
||||
|
||||
## Adding a Custom Reverse Proxy
|
||||
|
||||
Users can deploy a custom reverse proxy that forwards requests to Immich's reverse proxy. This way, the new reverse proxy can handle TLS termination, load balancing, or other advanced features, while still delegating routing decisions to Immich's reverse proxy. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
||||
|
||||
### Nginx example config
|
||||
|
||||
|
@ -28,3 +43,7 @@ server {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Replacing the Default Reverse Proxy
|
||||
|
||||
Replacing Immich's default reverse proxy is an advanced deployment and support may be limited. When replacing Immich's default proxy it is important to ensure that requests to `/api/*` are routed to the server container and all other requests to the web container. Additionally, the previously mentioned headers should be configured accordingly. You may find our [nginx configuration file](https://github.com/immich-app/immich/blob/main/nginx/templates/default.conf.template) a helpful reference.
|
||||
|
|
|
@ -51,7 +51,8 @@ immich-admin list-users
|
|||
{
|
||||
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
||||
email: 'immich@example.com.com',
|
||||
name: 'Immich Admin',
|
||||
firstName: 'Immich',
|
||||
lastName: 'Admin',
|
||||
storageLabel: 'admin',
|
||||
externalPath: null,
|
||||
profileImagePath: 'upload/profile/e65e6f88-2a30-4dbe-8dd9-1885f4889b53/e65e6f88-2a30-4dbe-8dd9-1885f4889b53.jpg',
|
||||
|
|
|
@ -34,7 +34,7 @@ The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses
|
|||
|
||||
### CLI
|
||||
|
||||
The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/docs/features/command-line-interface.md) for more information.
|
||||
The CLI is a [TypeScript](https://www.typescriptlang.org/) project that parses command line arguments to programmatically upload/import assets to an Immich server. See [Bulk Upload](/docs/features/bulk-upload.md) for more information about its usage.
|
||||
|
||||
## Server
|
||||
|
||||
|
|
|
@ -9,6 +9,6 @@ npm run typeorm:migrations:generate ./src/infra/<migration-name>
|
|||
```
|
||||
|
||||
2. Check if the migration file makes sense.
|
||||
3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
|
||||
3. Move the migration file to folder `./src/infra/database/migrations` in your code editor.
|
||||
|
||||
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
||||
|
|
|
@ -17,5 +17,6 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
|
|||
| `machine-learning/` | Source code for the `immich-machine-learning` docker image |
|
||||
| `misc/release/` | Scripts for version pumps and draft releases |
|
||||
| `mobile/` | Source code for the mobile app, both Android and iOS |
|
||||
| `nginx/` | Source code for the `immich-proxy` docker image |
|
||||
| `server/` | Source code for the `immich-server` docker image |
|
||||
| `web/` | Source code for the `web` |
|
||||
| `web/` | Source code for the `immich-web` docker image |
|
||||
|
|
|
@ -4,7 +4,7 @@ Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generat
|
|||
|
||||
## Generator
|
||||
|
||||
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server **when running in development mode**. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the server in development mode and run the command below to update the client SDK.
|
||||
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server when running in development mode. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the command below to update the client SDK.
|
||||
|
||||
```bash
|
||||
npm run api:generate # Run from the `server/` directory
|
||||
|
|
|
@ -52,7 +52,7 @@ If you only want to do web development connected to an existing, remote backend,
|
|||
3. Start the web development server
|
||||
|
||||
```
|
||||
IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev
|
||||
PUBLIC_IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev
|
||||
```
|
||||
|
||||
## IDE setup
|
||||
|
@ -61,15 +61,9 @@ IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev
|
|||
|
||||
Setting these in the IDE give a better developer experience, auto-formatting code on save, and providing instant feedback on lint issues.
|
||||
|
||||
### Dart Code Metris
|
||||
|
||||
The mobile app uses DCM (Dart Code Metrics) for linting and metrics calculation. Please refer to the [Getting Started](https://dcm.dev/docs/getting-started/#installation) page for more information on setting up DCM
|
||||
|
||||
Note: Activating the license is not required.
|
||||
|
||||
### VSCode
|
||||
|
||||
Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions.
|
||||
Install `Flutter`, `Prettier`, `ESLint` and `Svelte` extensions.
|
||||
|
||||
in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JSON`) add the following:
|
||||
|
||||
|
|
|
@ -12,6 +12,6 @@ The backend has an end-to-end test suite that can be called with `npm run test:e
|
|||
|
||||
Note that there is a bug in nodejs <20.8 that causes segmentation faults when running these tests. If you run into segfaults, ensure you are using at least version 20.8.
|
||||
|
||||
To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perform the tests and exit.
|
||||
To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perfom the tests and exit.
|
||||
|
||||
If you manually install the dependencies (see the DOCKERFILE) on your development machine, you can also run the full e2e tests manually by setting the `IMMICH_RUN_ALL_TESTS` environment value to true, i.e. `IMMICH_RUN_ALL_TESTS=true npm run test:e2e`.
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# Troubleshooting
|
||||
|
||||
:::tip
|
||||
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.gg/D8JsnBEuKb) server, where we have a dedicated channel for `#contributing`.
|
||||
:::
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Running on Windows
|
||||
|
||||
Running Immich on Windows can be frustrating and there are lots of ways it can go wrong. Where possible we recommend using Docker on Linux. However, several people have had success running Immich on Windows using Docker via WSL2.
|
||||
|
||||
### NTFS Mounted Volumes
|
||||
|
||||
The docker-compose.dev.yml and docker-compose.prod.yml use volume mounts for the postgres database. On start-up, postgres will try to `chown` the data directory, but fail. See [this post](https://forums.docker.com/t/data-directory-var-lib-postgresql-data-pgdata-has-wrong-ownership/17963/24) for more information about this issue and possible solutions.
|
177
docs/docs/features/bulk-upload.md
Normal file
|
@ -0,0 +1,177 @@
|
|||
# Bulk Upload (Using the CLI)
|
||||
|
||||
You can use the CLI to upload an existing gallery to the Immich server
|
||||
|
||||
[Immich CLI Repository](https://github.com/immich-app/CLI)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 16 or above
|
||||
- Npm
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i -g immich
|
||||
```
|
||||
|
||||
Pre-installed on the `immich-server` container and can be easily accessed through
|
||||
|
||||
```
|
||||
immich
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------- | ------------------------------------------------------------------- |
|
||||
| --yes / -y | Assume yes on all interactive prompts |
|
||||
| --recursive / -r | Include subfolders |
|
||||
| --delete / -da | Delete local assets after upload |
|
||||
| --key / -k | User's API key |
|
||||
| --server / -s | Immich's server address |
|
||||
| --threads / -t | Number of threads to use (Default 5) |
|
||||
| --album/ -al | Create albums for assets based on the parent folder or a given name |
|
||||
| --import/ -i | Import gallery (assets are not uploaded) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
Specify user's credential, Immich's server address and port and the directory you would like to upload videos/photos from.
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg
|
||||
```
|
||||
|
||||
By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option:
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/
|
||||
```
|
||||
|
||||
### Obtain the API Key
|
||||
|
||||
The API key can be obtained in the user setting panel on the web interface.
|
||||
|
||||
![Obtain Api Key](./img/obtain-api-key.png)
|
||||
|
||||
---
|
||||
|
||||
### Run via Docker
|
||||
|
||||
You can run the CLI inside of a docker container to avoid needing to install anything.
|
||||
|
||||
:::caution Running inside Docker
|
||||
Be aware that as this runs inside a container, you need to mount the folder from which you want to import into the container.
|
||||
:::
|
||||
|
||||
```bash title="Upload current directory"
|
||||
cd /DIRECTORY/WITH/IMAGES
|
||||
docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
|
||||
```
|
||||
|
||||
```bash title="Upload target directory"
|
||||
docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
|
||||
```
|
||||
|
||||
```bash title="Create an alias"
|
||||
alias immich='docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest'
|
||||
immich upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
|
||||
```
|
||||
|
||||
:::tip Internal networking
|
||||
If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps:
|
||||
|
||||
1. Find the internal Docker network used by Immich via `docker network ls`.
|
||||
2. Adapt the above command to pass the `--network <immich_network>` argument to `docker run`, substituting `<immich_network>` with the result from step 1.
|
||||
3. Use `--server http://immich-server:3001` for the upload command instead of the external address.
|
||||
|
||||
```bash title="Upload to internal address"
|
||||
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://immich-server:3001
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Run from source
|
||||
|
||||
```bash title="Clone Repository"
|
||||
git clone https://github.com/immich-app/CLI
|
||||
```
|
||||
|
||||
```bash title="Install dependencies"
|
||||
npm install
|
||||
```
|
||||
|
||||
```bash title="Build the project"
|
||||
npm run build
|
||||
```
|
||||
|
||||
```bash title="Run the command"
|
||||
node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Importing existing libraries
|
||||
|
||||
If you do not wish to upload files into the server, existing files can be imported into the immich gallery through the use of the `--import` flag.
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/ --import
|
||||
```
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg --import
|
||||
```
|
||||
|
||||
The `immich-server` and `immich-microservices` containers must be able to access the files, or directories at the path referenced in the command. The directories referenced must be set under a user's `External Path` setting. More detailed instructions can be found [here](/docs/features/read-only-gallery).
|
||||
|
||||
:::tip Matching volume references
|
||||
The import command is most easily run on the machine running the immich service, as the path to the files on the machine running the command and the server much match identically.
|
||||
|
||||
If you are running immich within docker, the volume pointing to your existing library should be identical with your host machine.
|
||||
|
||||
```diff title="docker-compose.yml"
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /path/to/media:/path/to/media
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "microservices" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /path/to/media:/path/to/media
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
```
|
||||
|
||||
The proper command for above would be as shown below. You should have access to `/path/to/media` exactly on the environment the CLI command is being run on
|
||||
|
||||
```
|
||||
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
|
||||
```
|
||||
|
||||
If you are running the import using the docker command, please note that the volumes should point to the `/path/to/media` exactly on the environment the CLI command is being run on
|
||||
|
||||
```
|
||||
docker run -it --rm -v "/path/to/media:/path/to/media" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
|
||||
```
|
||||
|
||||
:::
|
|
@ -1,139 +0,0 @@
|
|||
# The Immich CLI
|
||||
|
||||
Immich has a CLI that allows you to perform certain actions from the command line. This CLI replaces the [legacy CLI](https://github.com/immich-app/CLI) that was previously available. The CLI is hosted in the [cli folder of the the main Immich github repository](https://github.com/immich-app/immich/tree/main/cli).
|
||||
|
||||
## Features
|
||||
|
||||
- Upload photos and videos to Immich
|
||||
- Check server version
|
||||
|
||||
More features are planned for the future.
|
||||
|
||||
:::tip Google Photos Takeout
|
||||
If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
|
||||
:::
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20.0 or above
|
||||
- Npm
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i -g @immich/cli
|
||||
```
|
||||
|
||||
NOTE: if you previously installed the legacy CLI, you will need to uninstall it first:
|
||||
|
||||
```bash
|
||||
npm uninstall -g immich
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
immich
|
||||
```
|
||||
|
||||
```
|
||||
Usage: immich [options] [command]
|
||||
|
||||
Immich command line interface
|
||||
|
||||
Options:
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
upload [options] [paths...] Upload assets
|
||||
server-info Display server information
|
||||
login-key [instanceUrl] [apiKey] Login using an API key
|
||||
logout Remove stored credentials
|
||||
help [command] display help for command
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
The upload command supports the following options:
|
||||
|
||||
```
|
||||
Usage: immich upload [options] [paths...]
|
||||
|
||||
Upload assets
|
||||
|
||||
Arguments:
|
||||
paths One or more paths to assets to be uploaded
|
||||
|
||||
Options:
|
||||
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
||||
-i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS)
|
||||
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
||||
--help display help for command
|
||||
```
|
||||
|
||||
Note that the above options can read from environment variables as well.
|
||||
|
||||
## Quick Start
|
||||
|
||||
You begin by authenticating to your Immich server.
|
||||
|
||||
```bash
|
||||
immich login-key [instanceUrl] [apiKey]
|
||||
```
|
||||
|
||||
For instance,
|
||||
|
||||
```bash
|
||||
immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG
|
||||
```
|
||||
|
||||
This will store your credentials in a file in your home directory. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
|
||||
|
||||
Once you are authenticated, you can upload assets to your Immich server.
|
||||
|
||||
```bash
|
||||
immich upload file1.jpg file2.jpg
|
||||
```
|
||||
|
||||
By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option:
|
||||
|
||||
```bash
|
||||
immich upload --recursive directory/
|
||||
```
|
||||
|
||||
If you are unsure what will happen, you can use the `--dry-run` option to see what would happen without actually performing any actions.
|
||||
|
||||
```bash
|
||||
immich upload --dry-run --recursive directory/
|
||||
```
|
||||
|
||||
By default, the upload command will hash the files before uploading them. This is to avoid uploading the same file multiple times. If you are sure that the files are unique, you can skip this step by passing the `--skip-hash` option. Note that Immich always performs its own deduplication through hashing, so this is merely a performance consideration. If you have good bandwidth it might be faster to skip hashing.
|
||||
|
||||
```bash
|
||||
immich upload --skip-hash --recursive directory/
|
||||
```
|
||||
|
||||
You can automatically create albums based on the folder name by passing the `--album` option. This will automatically create albums for each uploaded asset based on the name of the folder they are in.
|
||||
|
||||
```bash
|
||||
immich upload --album --recursive directory/
|
||||
```
|
||||
|
||||
It is possible to skip assets matching a glob pattern by passing the `--ignore` option. See [the library documentation](docs/features/libraries.md) on how to use glob patterns. You can add several exclusion patterns if needed.
|
||||
|
||||
```bash
|
||||
immich upload --ignore **/Raw/** --recursive directory/
|
||||
```
|
||||
|
||||
```bash
|
||||
immich upload --ignore **/Raw/** **/*.tif --recursive directory/
|
||||
```
|
||||
|
||||
### Obtain the API Key
|
||||
|
||||
The API key can be obtained in the user setting panel on the web interface.
|
||||
|
||||
![Obtain Api Key](./img/obtain-api-key.png)
|
|
@ -1,7 +1,5 @@
|
|||
# Facial Recognition
|
||||
|
||||
## Overview
|
||||
|
||||
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
|
||||
|
||||
The list of people is shown in the Explore page.
|
||||
|
@ -15,16 +13,3 @@ Upon clicking on a person, a list of assets that contain their face will be show
|
|||
The asset detail view will also show the faces that are recognized in the asset.
|
||||
|
||||
<img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' />
|
||||
|
||||
## Actions
|
||||
|
||||
Additional actions you can do with a detected person are:
|
||||
|
||||
- Change the feature face photo of the person
|
||||
- Set date of birth
|
||||
- Merge two or more detected faces into one person
|
||||
- Hide face
|
||||
|
||||
It can be found from the app bar when you access the detial view of a person
|
||||
|
||||
<img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/>
|
||||
|
|
Before Width: | Height: | Size: 416 KiB |
|
@ -75,7 +75,7 @@ Some basic examples:
|
|||
- `*.tif` will exclude all files with the extension `.tif`
|
||||
- `hidden.jpg` will exclude all files named `hidden.jpg`
|
||||
- `**/Raw/**` will exclude all files in any directory named `Raw`
|
||||
- `*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
|
||||
- `*.(tif,jpg)` will exclude all files with the extension `.tif` or `.jpg`
|
||||
|
||||
### Nightly job
|
||||
|
||||
|
@ -85,7 +85,7 @@ There is an automatic job that's run once a day and refreshes all modified files
|
|||
|
||||
Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:
|
||||
|
||||
- `/home/user/old-pics`: a folder containing childhood photos.
|
||||
- `/home/user/old-pics`: a folder contining childhood photos.
|
||||
- `/mnt/nas/christmas-trip`: photos from a christmas trip. The subfolder `/mnt/nas/christmas-trip/Raw` contains the raw files directly from the DSLR. We don't want to import the raw files to Immich
|
||||
- `/mnt/media/videos`: Videos from the same christmas trip.
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import MobileAppDownload from '../partials/_mobile-app-download.md';
|
||||
import MobileAppLogin from '../partials/_mobile-app-login.md';
|
||||
import MobileAppBackup from '../partials/_mobile-app-backup.md';
|
||||
import MobileAppBackup from '../partials/_mobile-app-login.md';
|
||||
|
||||
# Mobile App
|
||||
|
||||
:::tip
|
||||
To upload from other devices, try using the [Bulk Upload CLI](/docs/features/bulk-upload.md).
|
||||
:::
|
||||
|
||||
## Download
|
||||
|
||||
<MobileAppDownload />
|
||||
|
|
97
docs/docs/features/read-only-gallery.md
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Read-only Gallery [Deprecated]
|
||||
|
||||
:::caution
|
||||
|
||||
This feature is being deprecated in favor of [Libraries](/docs/features/libraries.md).
|
||||
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
This feature enables users to use an existing gallery without uploading the assets to Immich.
|
||||
|
||||
Upon syncing the file information, it will be read by Immich to generate supported files.
|
||||
|
||||
## Usage
|
||||
|
||||
:::tip Example scenario
|
||||
|
||||
On the VM/system that Immich is running, I have 2 galleries that I want to use with Immich.
|
||||
|
||||
- My gallery is stored at `/mnt/media/precious-memory`
|
||||
- My wife's gallery is stored at `/mnt/media/childhood-memory`
|
||||
|
||||
We will use those values in the steps below.
|
||||
|
||||
:::
|
||||
|
||||
### Mount the gallery to the containers.
|
||||
|
||||
`immich-server` and `immich-microservices` containers will need access to the gallery. Mount the directory path as in the example below
|
||||
|
||||
```diff title="docker-compose.yml"
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "immich" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /mnt/media/precious-memory:/mnt/media/precious-memory:ro
|
||||
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "microservices" ]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - /mnt/media/precious-memory:/mnt/media/precious-memory:ro
|
||||
+ - /mnt/media/childhood-memory:/mnt/media/childhood-memory:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
- typesense
|
||||
restart: always
|
||||
```
|
||||
|
||||
:::tip
|
||||
Internal and external path have to be identical.
|
||||
:::
|
||||
|
||||
_Remember to bring the container down/up to register the changes. Make sure you can see the mounted path in the container._
|
||||
|
||||
### Register the path for the user.
|
||||
|
||||
This action is done by the admin of the instance.
|
||||
|
||||
- Navigate to `Administration > Users` page on the web.
|
||||
- Click on the user edit button.
|
||||
- Add the gallery path to the `External Path` field for the corresponding user and confirm the changes.
|
||||
|
||||
<img src={require('./img/me.png').default} width='33%' title='My Account Storage Path' />
|
||||
|
||||
<img src={require('./img/my-wife.png').default} width='33%' title='My Wifes Account Storage Path' />
|
||||
|
||||
### Sync with the CLI tool.
|
||||
|
||||
- Install or update the [CLI Tool](/docs/features/bulk-upload.md). The import feature is supported from version `v0.39.0` of the CLI
|
||||
- Run the command below to sync the gallery with Immich.
|
||||
|
||||
```bash title="Import my gallery"
|
||||
immich upload --key <my-api-key> --server http://my-server-ip:2283/api /mnt/media/precious-memory --recursive --import
|
||||
```
|
||||
|
||||
```bash title="Import my wife gallery"
|
||||
immich upload --key <my-wife-api-key> --server http://my-server-ip:2283/api /mnt/media/childhood-memory --recursive --import
|
||||
```
|
||||
|
||||
The `--import` flag will tell Immich to import the files by path instead of uploading them.
|
|
@ -14,6 +14,8 @@ 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
|
||||
|
@ -24,6 +26,8 @@ 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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
To alleviate [performance issues on low-memory systems](/docs/FAQ.md#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer):
|
||||
|
||||
- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
|
||||
- Set `IMMICH_MACHINE_LEARNING_URL` to point to the designated ML system, e.g. `http://workstation:3003`.
|
||||
- Copy the following `docker-compose.yml` to your ML system.
|
||||
- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).
|
||||
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
# Python File Upload
|
||||
|
||||
```python
|
||||
#!/usr/bin/python3
|
||||
|
||||
import requests
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
API_KEY = 'YOUR_API_KEY' # replace with a valid api key
|
||||
BASE_URL = 'http://127.0.0.1:2283/api' # replace as needed
|
||||
|
||||
|
||||
def upload(file):
|
||||
stats = os.stat(file)
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'x-api-key': API_KEY
|
||||
}
|
||||
|
||||
data = {
|
||||
'deviceAssetId': f'{file}-{stats.st_mtime}',
|
||||
'deviceId': 'python',
|
||||
'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime),
|
||||
'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime),
|
||||
'isFavorite': 'false',
|
||||
}
|
||||
|
||||
files = {
|
||||
'assetData': open(file, 'rb')
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f'{BASE_URL}/asset/upload', headers=headers, data=data, files=files)
|
||||
|
||||
print(response.json())
|
||||
# {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False}
|
||||
|
||||
|
||||
upload('./test.jpg')
|
||||
```
|
|
@ -1,58 +0,0 @@
|
|||
# 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.
|
|
@ -17,12 +17,6 @@ The default configuration looks like this:
|
|||
"targetAudioCodec": "aac",
|
||||
"targetResolution": "720",
|
||||
"maxBitrate": "0",
|
||||
"bframes": -1,
|
||||
"refs": 0,
|
||||
"gopSize": 0,
|
||||
"npl": 0,
|
||||
"temporalAQ": false,
|
||||
"cqMode": "auto",
|
||||
"twoPass": false,
|
||||
"transcode": "required",
|
||||
"tonemap": "hable",
|
||||
|
@ -50,15 +44,9 @@ The default configuration looks like this:
|
|||
"sidecar": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"library": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"storageTemplateMigration": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"migration": {
|
||||
"concurrency": 5
|
||||
},
|
||||
"thumbnailGeneration": {
|
||||
"concurrency": 5
|
||||
},
|
||||
|
@ -67,16 +55,16 @@ The default configuration looks like this:
|
|||
}
|
||||
},
|
||||
"machineLearning": {
|
||||
"classification": {
|
||||
"minScore": 0.7,
|
||||
"enabled": true,
|
||||
"modelName": "microsoft/resnet-50"
|
||||
},
|
||||
"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"
|
||||
"modelName": "ViT-B-32::openai"
|
||||
},
|
||||
"facialRecognition": {
|
||||
"enabled": true,
|
||||
|
@ -86,14 +74,6 @@ The default configuration looks like this:
|
|||
"minFaces": 1
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"enabled": true,
|
||||
"tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
},
|
||||
"reverseGeocoding": {
|
||||
"enabled": true,
|
||||
"citiesFileOverride": "cities500"
|
||||
},
|
||||
"oauth": {
|
||||
"enabled": false,
|
||||
"issuerUrl": "",
|
||||
|
@ -116,27 +96,8 @@ The default configuration looks like this:
|
|||
"thumbnail": {
|
||||
"webpSize": 250,
|
||||
"jpegSize": 1440,
|
||||
"quality": 80,
|
||||
"quality": 90,
|
||||
"colorspace": "p3"
|
||||
},
|
||||
"newVersionCheck": {
|
||||
"enabled": true
|
||||
},
|
||||
"trash": {
|
||||
"enabled": true,
|
||||
"days": 30
|
||||
},
|
||||
"theme": {
|
||||
"customCss": ""
|
||||
},
|
||||
"library": {
|
||||
"scan": {
|
||||
"enabled": true,
|
||||
"cronExpression": "0 0 * * *"
|
||||
}
|
||||
},
|
||||
"stylesheets": {
|
||||
"css": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -122,6 +122,28 @@ TYPESENSE_API_KEY=some-random-text
|
|||
|
||||
PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
|
||||
|
||||
####################################################################################
|
||||
# Alternative Service Addresses - Optional
|
||||
#
|
||||
# This is an advanced feature for users who may be running their immich services on different hosts.
|
||||
# It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers.
|
||||
# Note: immich-microservices is bound to 3002, but no references are made
|
||||
####################################################################################
|
||||
|
||||
IMMICH_WEB_URL=http://immich-web:3000
|
||||
IMMICH_SERVER_URL=http://immich-server:3001
|
||||
|
||||
####################################################################################
|
||||
# Alternative API's External Address - Optional
|
||||
#
|
||||
# This is an advanced feature used to control the public server endpoint returned to clients during Well-known discovery.
|
||||
# You should only use this if you want mobile apps to access the immich API over a custom URL. Do not include trailing slash.
|
||||
# NOTE: At this time, the web app will not be affected by this setting and will continue to use the relative path: /api
|
||||
# Examples: http://localhost:3001, http://immich-api.example.com, etc
|
||||
####################################################################################
|
||||
|
||||
#IMMICH_API_URL_EXTERNAL=http://localhost:3001
|
||||
|
||||
###################################################################################
|
||||
# Immich Version - Optional
|
||||
#
|
||||
|
@ -144,7 +166,7 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
|
|||
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
|
||||
docker-compose up -d # or `docker compose up -d` based on your docker-compose version
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
@ -162,7 +184,7 @@ If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired
|
|||
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
|
||||
docker-compose pull && docker-compose up -d # Or `docker compose up -d`
|
||||
```
|
||||
|
||||
:::caution Automatic Updates
|
||||
|
|
|
@ -63,6 +63,21 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
|
||||
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
|
||||
|
||||
## URLs
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :------------------------- | :---------------------- | :-------------------------: | :--------- |
|
||||
| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy |
|
||||
| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy |
|
||||
| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web |
|
||||
| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web |
|
||||
|
||||
:::info
|
||||
|
||||
The above paths are modifying the internal paths of the containers.
|
||||
|
||||
:::
|
||||
|
||||
## Database
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
|
@ -173,18 +188,19 @@ Typesense URL example JSON before encoding:
|
|||
|
||||
| Variable | Description | Default | Services |
|
||||
| :----------------------------------------------- | :---------------------------------------------------------------- | :-----------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if <= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if <= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL`<sup>\*1</sup> | Inactivity time (s) before a model is unloaded (disabled if <= 0) | `0` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if <= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*2</sup> | Thread count of the request thread pool (disabled if <= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*3</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
|
||||
|
||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||
\*1: This is an experimental feature. It may result in increased memory use over time when loading models repeatedly.
|
||||
|
||||
\*2: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around.
|
||||
\*2: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||
|
||||
\*3: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around.
|
||||
|
||||
:::info
|
||||
|
||||
|
|
|
@ -98,12 +98,12 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
|||
|
||||
> Note: This can take several minutes depending on your Internet speed and Unraid hardware
|
||||
|
||||
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_web` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
|
||||
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_proxy` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
|
||||
|
||||
<img
|
||||
src={require('./img/unraid06.webp').default}
|
||||
width="80%"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-proxy"
|
||||
/>
|
||||
|
||||
<details >
|
||||
|
@ -112,12 +112,12 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
|
|||
<img
|
||||
src={require('./img/unraid07.webp').default}
|
||||
width="80%"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-proxy"
|
||||
/>
|
||||
<img
|
||||
src={require('./img/unraid08.webp').default}
|
||||
width="90%"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
alt="Go to Docker Tab and visit the address listed next to immich-proxy"
|
||||
/>
|
||||
|
||||
</details>
|
||||
|
|
|
@ -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/immich-app)
|
||||
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/immich-app?frequency=one-time)
|
||||
- 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)
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
|
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 321 KiB |
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 335 KiB |
152
docs/package-lock.json
generated
|
@ -15,7 +15,7 @@
|
|||
"@mdx-js/react": "^1.6.22",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"classnames": "^2.3.2",
|
||||
"clsx": "^2.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-lunr-search": "^2.3.2",
|
||||
"docusaurus-preset-openapi": "^0.6.3",
|
||||
"postcss": "^8.4.25",
|
||||
|
@ -28,7 +28,7 @@
|
|||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2603,14 +2603,6 @@
|
|||
"react-dom": "^16.8.4 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-classic/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-common": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz",
|
||||
|
@ -2641,14 +2633,6 @@
|
|||
"react-dom": "^16.8.4 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-common/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-search-algolia": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz",
|
||||
|
@ -2679,14 +2663,6 @@
|
|||
"react-dom": "^16.8.4 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-search-algolia/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-translations": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz",
|
||||
|
@ -4972,9 +4948,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -6019,14 +5995,6 @@
|
|||
"react-dom": "^16.8.4 || ^17"
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-lunr-search/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-plugin-openapi": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz",
|
||||
|
@ -6057,14 +6025,6 @@
|
|||
"react-dom": "^16.8.4 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-plugin-openapi/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
|
@ -6138,14 +6098,6 @@
|
|||
"react-dom": "^16.8.4 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-theme-openapi/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-converter": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||
|
@ -10849,15 +10801,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
|
@ -13280,19 +13232,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.19.1",
|
||||
"jiti": "^1.18.2",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
|
@ -13614,9 +13566,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
|
||||
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -16793,13 +16745,6 @@
|
|||
"rtlcss": "^3.5.0",
|
||||
"tslib": "^2.4.0",
|
||||
"utility-types": "^3.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@docusaurus/theme-common": {
|
||||
|
@ -16823,13 +16768,6 @@
|
|||
"tslib": "^2.4.0",
|
||||
"use-sync-external-store": "^1.2.0",
|
||||
"utility-types": "^3.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@docusaurus/theme-search-algolia": {
|
||||
|
@ -16853,13 +16791,6 @@
|
|||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.4.0",
|
||||
"utility-types": "^3.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@docusaurus/theme-translations": {
|
||||
|
@ -18584,9 +18515,9 @@
|
|||
}
|
||||
},
|
||||
"clsx": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "1.0.6",
|
||||
|
@ -19312,13 +19243,6 @@
|
|||
"to-vfile": "^6.1.0",
|
||||
"unified": "^9.0.0",
|
||||
"unist-util-is": "^4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"docusaurus-plugin-openapi": {
|
||||
|
@ -19344,11 +19268,6 @@
|
|||
"webpack": "^5.73.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
|
@ -19402,13 +19321,6 @@
|
|||
"react-redux": "^7.2.0",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"webpack": "^5.73.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-converter": {
|
||||
|
@ -22751,9 +22663,9 @@
|
|||
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-error": {
|
||||
|
@ -24605,19 +24517,19 @@
|
|||
}
|
||||
},
|
||||
"tailwindcss": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||
"requires": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.19.1",
|
||||
"jiti": "^1.18.2",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
|
@ -24845,9 +24757,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
|
||||
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ=="
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "1.0.36",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"@mdx-js/react": "^1.6.22",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"classnames": "^2.3.2",
|
||||
"clsx": "^2.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-lunr-search": "^2.3.2",
|
||||
"docusaurus-preset-openapi": "^0.6.3",
|
||||
"postcss": "^8.4.25",
|
||||
|
@ -37,7 +37,7 @@
|
|||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"browserslist": {
|
||||
|
|
|
@ -34,7 +34,7 @@ function HomepageHeader() {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
<img src="/img/immich-screenshots.png" alt="screenshots" width={'85%'} />
|
||||
<img src="/img/immich-screenshots.png" alt="logo" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
|
||||
<div className="h-24">
|
||||
|
|
|
@ -3,20 +3,17 @@ import {
|
|||
mdiAndroid,
|
||||
mdiAppleIos,
|
||||
mdiArchiveOutline,
|
||||
mdiBash,
|
||||
mdiBookSearchOutline,
|
||||
mdiCakeVariant,
|
||||
mdiCheckAll,
|
||||
mdiCheckboxMarked,
|
||||
mdiCollage,
|
||||
mdiContentCopy,
|
||||
mdiDevices,
|
||||
mdiFaceMan,
|
||||
mdiFaceManOutline,
|
||||
mdiFile,
|
||||
mdiFileSearch,
|
||||
mdiFolder,
|
||||
mdiForum,
|
||||
mdiHeart,
|
||||
mdiImage,
|
||||
mdiImageAlbum,
|
||||
|
@ -29,7 +26,6 @@ import {
|
|||
mdiMerge,
|
||||
mdiMonitor,
|
||||
mdiMotionPlayOutline,
|
||||
mdiPalette,
|
||||
mdiPanVertical,
|
||||
mdiPartyPopper,
|
||||
mdiRaw,
|
||||
|
@ -43,7 +39,6 @@ import {
|
|||
mdiText,
|
||||
mdiThemeLightDark,
|
||||
mdiTrashCanOutline,
|
||||
mdiVectorCombine,
|
||||
mdiVideo,
|
||||
mdiWeb,
|
||||
} from '@mdi/js';
|
||||
|
@ -52,61 +47,6 @@ import React from 'react';
|
|||
import Timeline, { DateType, Item } from '../components/timeline';
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiVectorCombine,
|
||||
description:
|
||||
'The serving of the web app is merged into the server image, allowing us to remove two containers from the stack.',
|
||||
title: 'Container consolidation',
|
||||
release: 'v1.88.0',
|
||||
tag: 'v1.88.0',
|
||||
date: new Date(2023, 10, 20),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiBash,
|
||||
description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.',
|
||||
title: 'CLI v2',
|
||||
release: 'v1.88.0',
|
||||
tag: 'v1.88.0',
|
||||
date: new Date(2023, 10, 19),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiForum,
|
||||
description: 'Comment a photo or a video in a shared album',
|
||||
title: 'Activity',
|
||||
release: 'v1.84.0',
|
||||
tag: 'v1.84.0',
|
||||
date: new Date(2023, 10, 1),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiStar,
|
||||
description: 'Reach 20K Stars on GitHub!',
|
||||
title: '20,000 Stars',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiContentCopy,
|
||||
title: 'Stack assets',
|
||||
description: 'Manual asset stacking for grouping and hiding related assets in the main timeline.',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiPalette,
|
||||
title: 'Custom theme',
|
||||
description: 'Apply your custom CSS for modifying fonts, colors, and styles in the web application.',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiTrashCanOutline,
|
||||
title: 'Trash Feature',
|
||||
|
@ -343,7 +283,7 @@ const items: Item[] = [
|
|||
},
|
||||
{
|
||||
icon: mdiStar,
|
||||
description: 'Reach 10K Stars on GitHub!',
|
||||
description: 'Reach 10K Starts on GitHub!',
|
||||
title: '10,000 Stars',
|
||||
release: 'v1.54.0',
|
||||
tag: 'v1.54.0',
|
||||
|
|
|
@ -61,12 +61,8 @@
|
|||
.searchbox__input {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
-webkit-transition:
|
||||
box-shadow 0.4s ease,
|
||||
background 0.4s ease;
|
||||
transition:
|
||||
box-shadow 0.4s ease,
|
||||
background 0.4s ease;
|
||||
-webkit-transition: box-shadow 0.4s ease, background 0.4s ease;
|
||||
transition: box-shadow 0.4s ease, background 0.4s ease;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: inset 0 0 0 1px #cccccc;
|
||||
|
@ -247,9 +243,7 @@
|
|||
}
|
||||
|
||||
.algolia-autocomplete .ds-dropdown-menu {
|
||||
box-shadow:
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2),
|
||||
0 2px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (min-width: 601px) {
|
||||
|
|
BIN
docs/static/img/immich-screenshots.png
vendored
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.6 MiB |
|
@ -12,8 +12,7 @@
|
|||
{ "source": "/docs/overview/logo-meaning", "destination": "/docs/overview/logo" },
|
||||
{ "source": "/docs/overview/technology-stack", "destination": "/docs/developer/architecture" },
|
||||
{ "source": "/docs/usage/automatic-backup", "destination": "/docs/features/automatic-backup" },
|
||||
{ "source": "/docs/usage/bulk-upload", "destination": "/docs/features/command-line-interface" },
|
||||
{ "source": "/docs/features/bulk-upload", "destination": "/docs/features/command-line-interface" },
|
||||
{ "source": "/docs/usage/bulk-upload", "destination": "/docs/features/bulk-upload" },
|
||||
{ "source": "/docs/usage/oauth", "destination": "/docs/administration/oauth" },
|
||||
{ "source": "/docs/usage/post-installation", "destination": "/docs/install/post-install" },
|
||||
{ "source": "/docs/usage/update", "destination": "/docs/install/docker-compose#step-4---upgrading" },
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-bookworm@sha256:ba7a7ac30c38e119c4304f98ef0e188f90f4f67a958bb6899da9defb99bfb471 as builder
|
||||
FROM python:3.11-bookworm as builder
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
|
@ -10,10 +10,11 @@ RUN poetry config installer.max-workers 10 && \
|
|||
RUN python -m venv /opt/venv
|
||||
ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
||||
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
COPY poetry.lock pyproject.toml requirements.txt ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
RUN pip install --no-deps -r requirements.txt
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:cc758519481092eb5a4a5ab0c1b303e288880d59afc601958d19e95b300bc86b
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
|
@ -13,8 +13,7 @@ from .schemas import ModelType
|
|||
|
||||
class Settings(BaseSettings):
|
||||
cache_folder: str = "/cache"
|
||||
model_ttl: int = 300
|
||||
model_ttl_poll_s: int = 10
|
||||
model_ttl: int = 0
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 3003
|
||||
workers: int = 1
|
||||
|
@ -22,6 +21,8 @@ class Settings(BaseSettings):
|
|||
request_threads: int = os.cpu_count() or 4
|
||||
model_inter_op_threads: int = 1
|
||||
model_intra_op_threads: int = 2
|
||||
max_batch_size: int = 1
|
||||
batch_timeout_s: float = 0.005
|
||||
|
||||
class Config:
|
||||
env_prefix = "MACHINE_LEARNING_"
|
||||
|
@ -39,16 +40,8 @@ class LogSettings(BaseSettings):
|
|||
_clean_name = str.maketrans(":\\/", "___", ".")
|
||||
|
||||
|
||||
def clean_name(model_name: str) -> str:
|
||||
return model_name.split("/")[-1].translate(_clean_name)
|
||||
|
||||
|
||||
def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
|
||||
return Path(settings.cache_folder) / model_type.value / clean_name(model_name)
|
||||
|
||||
|
||||
def get_hf_model_name(model_name: str) -> str:
|
||||
return f"immich-app/{clean_name(model_name)}"
|
||||
return Path(settings.cache_folder) / model_type.value / model_name.translate(_clean_name)
|
||||
|
||||
|
||||
LOG_LEVELS: dict[str, int] = {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from typing import Any, Iterator, TypeAlias
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
@ -9,7 +8,8 @@ from fastapi.testclient import TestClient
|
|||
from PIL import Image
|
||||
|
||||
from .main import app, init_state
|
||||
from .schemas import ndarray_f32
|
||||
|
||||
ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -18,13 +18,13 @@ def pil_image() -> Image.Image:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def cv_image(pil_image: Image.Image) -> ndarray_f32:
|
||||
def cv_image(pil_image: Image.Image) -> ndarray:
|
||||
return np.asarray(pil_image)[:, :, ::-1] # PIL uses RGB while cv2 uses BGR
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_model() -> Iterator[mock.Mock]:
|
||||
with mock.patch("app.models.cache.from_model_type", autospec=True) as mocked:
|
||||
with mock.patch("app.models.cache.InferenceModel.from_model_type", autospec=True) as mocked:
|
||||
yield mocked
|
||||
|
||||
|
||||
|
@ -36,27 +36,4 @@ def deployed_app() -> TestClient:
|
|||
|
||||
@pytest.fixture(scope="session")
|
||||
def responses() -> dict[str, Any]:
|
||||
responses: dict[str, Any] = json.load(open("responses.json", "r"))
|
||||
return responses
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clip_model_cfg() -> dict[str, Any]:
|
||||
return {
|
||||
"embed_dim": 512,
|
||||
"vision_cfg": {"image_size": 224, "layers": 12, "width": 768, "patch_size": 32},
|
||||
"text_cfg": {"context_length": 77, "vocab_size": 49408, "width": 512, "heads": 8, "layers": 12},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clip_preprocess_cfg() -> dict[str, Any]:
|
||||
return {
|
||||
"size": [224, 224],
|
||||
"mode": "RGB",
|
||||
"mean": [0.48145466, 0.4578275, 0.40821073],
|
||||
"std": [0.26862954, 0.26130258, 0.27577711],
|
||||
"interpolation": "bicubic",
|
||||
"resize_mode": "shortest",
|
||||
"fill_color": 0,
|
||||
}
|
||||
return json.load(open("responses.json", "r"))
|
||||
|
|