Compare commits

..

4 Commits

3758 changed files with 243186 additions and 459561 deletions

View File

@@ -1,67 +0,0 @@
{
"name": "Immich - Backend, Frontend and ML",
"service": "immich-server",
"runServices": [
"immich-server",
"redis",
"database",
"immich-machine-learning"
],
"dockerComposeFile": [
"../docker/docker-compose.dev.yml",
"./server/container-compose-overrides.yml"
],
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"svelte.svelte-vscode",
"ms-vscode-remote.remote-containers",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"rvest.vs-code-prettier-eslint",
"bluebrown.yamlfmt",
"vkrishna04.cspell-sync",
"vitest.explorer",
"ms-playwright.playwright",
"ms-azuretools.vscode-docker"
]
}
},
"forwardPorts": [3000, 9231, 9230, 2283],
"portsAttributes": {
"3000": {
"label": "Immich - Frontend HTTP",
"description": "The frontend of the Immich project",
"onAutoForward": "openBrowserOnce"
},
"2283": {
"label": "Immich - API Server - HTTP",
"description": "The API server of the Immich project"
},
"9231": {
"label": "Immich - API Server - DEBUG",
"description": "The API server of the Immich project"
},
"9230": {
"label": "Immich - Workers - DEBUG",
"description": "The workers of the Immich project"
}
},
"overrideCommand": true,
"workspaceFolder": "/workspaces/immich",
"remoteUser": "node",
"userEnvProbe": "loginInteractiveShell",
"remoteEnv": {
// The location where your uploaded files are stored
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./library}",
// Connection secret for postgres. You should change it to a random password
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
// The database username
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
// The database name
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
}
}

View File

@@ -1,33 +0,0 @@
services:
immich-server:
build:
target: dev-container-mobile
environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: !override # bind mount host to /workspaces/immich
- ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules
- e2e_node_modules:/workspaces/immich/e2e/node_modules
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro
database:
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
volumes:
# Node modules for each service to avoid conflicts and ensure consistent dependencies
cli_node_modules:
e2e_node_modules:
open_api_node_modules:
server_node_modules:
web_node_modules:
# UPLOAD_LOCATION must be set to a absolute path or vol-upload
vol-upload:
# DB_DATA_LOCATION must be set to a absolute path or vol-database
vol-database:

View File

@@ -1,52 +0,0 @@
{
"name": "Immich - Mobile",
"service": "immich-server",
"runServices": [
"immich-server",
"redis",
"database",
"immich-machine-learning"
],
"dockerComposeFile": [
"../../docker/docker-compose.dev.yml",
"./container-compose-overrides.yml"
],
"customizations": {
"vscode": {
"extensions": [
"Dart-Code.dart-code",
"Dart-Code.flutter",
"dcmdev.dcm-vscode-extension",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"svelte.svelte-vscode",
"ms-vscode-remote.remote-containers",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"rvest.vs-code-prettier-eslint",
"bluebrown.yamlfmt",
"vkrishna04.cspell-sync",
"vitest.explorer",
"ms-playwright.playwright",
"ms-azuretools.vscode-docker"
]
}
},
"forwardPorts": [],
"overrideCommand": true,
"workspaceFolder": "/workspaces/immich",
"remoteUser": "node",
"userEnvProbe": "loginInteractiveShell",
"remoteEnv": {
// The location where your uploaded files are stored
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
// Connection secret for postgres. You should change it to a random password
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",
// The database username
"DB_USERNAME": "${localEnv:DB_USERNAME:postgres}",
// The database name
"DB_DATABASE_NAME": "${localEnv:DB_DATABASE_NAME:immich}"
}
}

View File

@@ -1,81 +0,0 @@
#!/bin/bash
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
export DEV_PORT="${DEV_PORT:-3000}"
# search for immich directory inside workspace.
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
# Devcontainer: Clone [repository|pull request] in container volumne
WORKSPACES_DIR="/workspaces"
IMMICH_DIR="$WORKSPACES_DIR/immich"
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
log() {
# Display command on console, log with timestamp to file
echo "$*"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$IMMICH_DEVCONTAINER_LOG"
}
run_cmd() {
# Ensure log directory exists
mkdir -p "$(dirname "$IMMICH_DEVCONTAINER_LOG")"
log "$@"
# Execute command: display normally on console, log with timestamps to file
"$@" 2>&1 | tee >(while IFS= read -r line; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line" >>"$IMMICH_DEVCONTAINER_LOG"
done)
# Preserve exit status
return "${PIPESTATUS[0]}"
}
# Find directories excluding /workspaces/immich
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
if [ ${#other_dirs[@]} -gt 1 ]; then
log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR."
exit 1
elif [ ${#other_dirs[@]} -eq 1 ]; then
export IMMICH_WORKSPACE="${other_dirs[0]}"
else
export IMMICH_WORKSPACE="$IMMICH_DIR"
fi
log "Found immich workspace in $IMMICH_WORKSPACE"
log ""
fix_permissions() {
log "Fixing permissions for ${IMMICH_WORKSPACE}"
# Change ownership for directories that exist
for dir in "${IMMICH_WORKSPACE}/.vscode" \
"${IMMICH_WORKSPACE}/server/upload" \
"${IMMICH_WORKSPACE}/.pnpm-store" \
"${IMMICH_WORKSPACE}/.github/node_modules" \
"${IMMICH_WORKSPACE}/cli/node_modules" \
"${IMMICH_WORKSPACE}/e2e/node_modules" \
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
"${IMMICH_WORKSPACE}/server/node_modules" \
"${IMMICH_WORKSPACE}/server/dist" \
"${IMMICH_WORKSPACE}/web/node_modules" \
"${IMMICH_WORKSPACE}/web/dist"; do
if [ -d "$dir" ]; then
run_cmd sudo chown node -R "$dir"
fi
done
log ""
}
install_dependencies() {
log "Installing dependencies"
(
cd "${IMMICH_WORKSPACE}" || exit 1
export CI=1 FROZEN=1 OFFLINE=1
run_cmd make setup-web-dev setup-server-dev
)
log ""
}

View File

@@ -1,42 +0,0 @@
services:
immich-server:
build:
target: dev-container-server
env_file: !reset []
hostname: immich-dev
environment:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: !override
- ..:/workspaces/immich
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- /etc/localtime:/etc/localtime:ro
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
immich-web:
env_file: !reset []
immich-machine-learning:
env_file: !reset []
database:
env_file: !reset []
environment: !override
POSTGRES_PASSWORD: ${DB_PASSWORD-postgres}
POSTGRES_USER: ${DB_USERNAME-postgres}
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
POSTGRES_INITDB_ARGS: '--data-checksums'
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
redis:
env_file: !reset []
volumes:
upload-devcontainer-volume:
postgres-devcontainer-volume:

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
log "Preparing Immich Nest API Server"
log ""
export CI=1
run_cmd pnpm --filter immich install
log "Starting Nest API Server"
log ""
cd "${IMMICH_WORKSPACE}/server" || (
log "Immich workspace not found"
exit 1
)
while true; do
run_cmd pnpm --filter immich exec nest start --debug "0.0.0.0:9230" --watch
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
sleep 3
done

View File

@@ -1,29 +0,0 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
export CI=1
log "Preparing Immich Web Frontend"
log ""
run_cmd pnpm --filter @immich/sdk install
run_cmd pnpm --filter @immich/sdk build
run_cmd pnpm --filter immich-web install
log "Starting Immich Web Frontend"
log ""
cd "${IMMICH_WORKSPACE}/web" || (
log "Immich Workspace not found"
exit 1
)
until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_PORT}/api/server/config"; do
log "Waiting for api server..."
sleep 1
done
while true; do
run_cmd pnpm --filter immich-web exec vite dev --host 0.0.0.0 --port "${DEV_PORT}"
log "Web crashed with exit code $?. Respawning in 3s ..."
sleep 3
done

View File

@@ -1,17 +0,0 @@
#!/bin/bash
# shellcheck source=common.sh
# shellcheck disable=SC1091
source /immich-devcontainer/container-common.sh
log "Setting up Immich dev container..."
fix_permissions
log "Setup complete, please wait while backend and frontend services automatically start"
log
log "If necessary, the services may be manually started using"
log
log "$ /immich-devcontainer/container-start-backend.sh"
log "$ /immich-devcontainer/container-start-frontend.sh"
log
log "From different terminal windows, as these scripts automatically restart the server"
log "on error, and will continuously run in a loop"

View File

@@ -1,41 +1,32 @@
.vscode/ .vscode/
.github/ .github/
.git/ .git/
.env*
*.log
*.tmp
*.temp
**/Dockerfile
**/node_modules/
**/.pnpm-store/
**/dist/
**/coverage/
**/build/
design/ design/
docker/ docker/
!docker/scripts !docker/scripts
docs/ docs/
!docs/package.json
!docs/package-lock.json
e2e/ e2e/
!e2e/package.json
!e2e/package-lock.json
fastlane/ fastlane/
machine-learning/ machine-learning/
misc/ misc/
mobile/ mobile/
open-api/typescript-sdk/build/ cli/coverage/
!open-api/typescript-sdk/package.json cli/dist/
!open-api/typescript-sdk/package-lock.json cli/node_modules/
open-api/typescript-sdk/build/
open-api/typescript-sdk/node_modules/
server/coverage/
server/node_modules/
server/upload/ server/upload/
server/src/queries server/dist/
server/www/ server/www/
web/node_modules/
web/coverage/
web/.svelte-kit web/.svelte-kit
web/build/
web/.env

12
.gitattributes vendored
View File

@@ -6,18 +6,6 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true mobile/lib/**/*.g.dart linguist-generated=true
mobile/lib/**/*.drift.dart -diff -merge
mobile/lib/**/*.drift.dart linguist-generated=true
mobile/drift_schemas/main/drift_schema_*.json -diff -merge
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
mobile/lib/infrastructure/repositories/db.repository.steps.dart -diff -merge
mobile/lib/infrastructure/repositories/db.repository.steps.dart linguist-generated=true
mobile/test/drift/main/generated/** -diff -merge
mobile/test/drift/main/generated/** linguist-generated=true
open-api/typescript-sdk/fetch-client.ts -diff -merge open-api/typescript-sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true open-api/typescript-sdk/fetch-client.ts linguist-generated=true

1
.github/.nvmrc vendored
View File

@@ -1 +0,0 @@
22.19.0

View File

@@ -1,4 +0,0 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,5 +1,5 @@
title: '[Feature] feature-name-goes-here' title: "[Feature] feature-name-goes-here"
labels: ['feature'] labels: ["feature"]
body: body:
- type: markdown - type: markdown
@@ -11,9 +11,10 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request. label: I have searched the existing feature requests to make sure this is not a duplicate request.
options: options:
- label: 'Yes' - label: "Yes"
required: true
- type: textarea - type: textarea
id: feature id: feature

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
custom: ['https://buy.immich.app', 'https://immich.store']

View File

@@ -1,12 +1,6 @@
name: Report an issue with Immich name: Report an issue with Immich
description: Report an issue with Immich description: Report an issue with Immich
body: body:
- type: checkboxes
attributes:
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
options:
- label: 'Yes'
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
@@ -64,11 +58,6 @@ body:
- label: Web - label: Web
- label: Mobile - label: Mobile
- type: input
attributes:
label: Device make and model
placeholder: Samsung S25 Android 16
- type: textarea - type: textarea
validations: validations:
required: true required: true
@@ -88,12 +77,13 @@ body:
id: repro id: repro
attributes: attributes:
label: Reproduction steps label: Reproduction steps
description: 'How do you trigger this bug? Please walk us through it step by step.' description: "How do you trigger this bug? Please walk us through it step by step."
value: | value: |
1. 1.
2. 2.
3. 3.
... ...
render: bash
validations: validations:
required: true required: true
@@ -101,13 +91,12 @@ body:
id: logs id: logs
attributes: attributes:
label: Relevant log output label: Relevant log output
description: description: Please copy and paste any relevant logs below. (code formatting is
Please copy and paste any relevant logs below. (code formatting is
enabled, no need for backticks) enabled, no need for backticks)
render: shell render: shell
validations: validations:
required: false required: false
- type: textarea - type: textarea
attributes: attributes:
label: Additional information label: Additional information

View File

@@ -1,14 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: I have a question or need support - name: I have a question or need support
url: https://discord.immich.app url: https://discord.immich.app
about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support. about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support.
- name: 📷 My photo or video has a date, time, or timezone problem - name: Feature Request
url: https://github.com/immich-app/immich/discussions/12650
about: Upload a sample file to this discussion and we will take a look
- name: 🌟 Feature request
url: https://github.com/immich-app/immich/discussions/new?category=feature-request url: https://github.com/immich-app/immich/discussions/new?category=feature-request
about: Please use our GitHub Discussion for making feature requests. about: Please use our GitHub Discussion for making feature requests.
- name: 🫣 I'm unsure where to go - name: I'm unsure where to go
url: https://discord.immich.app url: https://discord.immich.app
about: If you are unsure where to go, then joining our Discord is recommended; Just ask! about: If you are unsure where to go, then joining our Discord is recommended; Just ask!

View File

@@ -1 +1,2 @@
blank_issues_enabled: false
blank_pull_request_template_enabled: false blank_pull_request_template_enabled: false

View File

@@ -0,0 +1,22 @@
## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes # (issue)
## How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
- [ ] Test A
- [ ] Test B
## Screenshots (if appropriate):
## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

4
.github/labeler.yml vendored
View File

@@ -6,6 +6,7 @@ cli:
documentation: documentation:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- docs/blob/**
- docs/docs/** - docs/docs/**
- docs/src/** - docs/src/**
- docs/static/** - docs/static/**
@@ -32,6 +33,3 @@ documentation:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- machine-learning/app/** - machine-learning/app/**
changelog:translation:
- head-branch: ['^chore/translations$']

View File

@@ -1,9 +0,0 @@
{
"scripts": {
"format": "prettier --check .",
"format:fix": "prettier --write ."
},
"devDependencies": {
"prettier": "^3.5.3"
}
}

View File

@@ -1,40 +0,0 @@
## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes # (issue)
## How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
- [ ] Test A
- [ ] Test B
<details><summary><h2>Screenshots (if appropriate)</h2></summary>
<!-- Images go below this line. -->
</details>
<!-- API endpoint changes (if relevant)
## API Changes
The `/api/something` endpoint is now `/api/something-else`
-->
## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable
- [ ] I have no unrelated changes in the PR.
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
## Please describe to which degree, if any, an LLM was used in creating this pull request.
...

74
.github/release.yml vendored
View File

@@ -1,33 +1,41 @@
changelog: changelog:
categories: categories:
- title: 🚨 Breaking Changes - title: ⚠️ Breaking Changes
labels: labels:
- changelog:breaking-change - breaking-change
- title: 🫥 Deprecated Changes - title: 🗄️ Server
labels: labels:
- changelog:deprecated - 🗄server
- title: 🔒 Security - title: 📱 Mobile
labels: labels:
- changelog:security - 📱mobile
- title: 🚀 Features - title: 🖥️ Web
labels: labels:
- changelog:feature - 🖥web
- title: 🌟 Enhancements - title: 🧠 Machine Learning
labels: labels:
- changelog:enhancement - 🧠machine-learning
- title: 🐛 Bug fixes - title: ⚡ CLI
labels: labels:
- changelog:bugfix - cli
- title: 📚 Documentation - title: 📓 Documentation
labels: labels:
- changelog:documentation - documentation
- title: 🌐 Translations - title: 🔨 Maintenance
labels: labels:
- changelog:translation - deployment
- dependencies
- renovate
- maintenance
- tech-debt
- title: Other changes
labels:
- "*"

View File

@@ -7,15 +7,6 @@ on:
ref: ref:
required: false required: false
type: string type: string
secrets:
KEY_JKS:
required: true
ALIAS:
required: true
ANDROID_KEY_PASSWORD:
required: true
ANDROID_STORE_PASSWORD:
required: true
pull_request: pull_request:
push: push:
branches: [main] branches: [main]
@@ -24,119 +15,61 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
mobile:
- 'mobile/**'
force-filters: |
- '.github/workflows/build-mobile.yml'
force-events: 'workflow_call,workflow_dispatch'
build-sign-android: build-sign-android:
name: Build and sign Android name: Build and sign Android
needs: pre-job
permissions:
contents: read
# Skip when PR from a fork # Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
runs-on: mich runs-on: macos-14
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Determine ref
id: get-ref
run: |
input_ref="${{ inputs.ref }}"
github_ref="${{ github.sha }}"
ref="${input_ref:-$github_ref}"
echo "ref=$ref" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4
with: with:
ref: ${{ inputs.ref || github.sha }} ref: ${{ steps.get-ref.outputs.ref }}
persist-credentials: false
- name: Create the Keystore - uses: actions/setup-java@v4
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
cache: 'gradle'
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/sdk
mobile/android/.gradle
mobile/.dart_tool
key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
cache: true cache: true
- name: Setup Android SDK - name: Create the Keystore
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 env:
with: KEY_JKS: ${{ secrets.KEY_JKS }}
packages: '' working-directory: ./mobile
run: echo $KEY_JKS | base64 -d > android/key.jks
- name: Get Packages - name: Get Packages
working-directory: ./mobile working-directory: ./mobile
run: flutter pub get run: flutter pub get
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
working-directory: ./mobile
- name: Generate platform APIs
run: make pigeon
working-directory: ./mobile
- name: Build Android App Bundle - name: Build Android App Bundle
working-directory: ./mobile working-directory: ./mobile
env: env:
ALIAS: ${{ secrets.ALIAS }} ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
run: | run: |
if [[ $IS_MAIN == 'true' ]]; then flutter build apk --release
flutter build apk --release flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
flutter build apk --debug --split-per-abi --target-platform android-arm64
fi
- name: Publish Android Artifact - name: Publish Android Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: release-apk-signed name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: github.ref == 'refs/heads/main'
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/sdk
mobile/android/.gradle
mobile/.dart_tool
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}

View File

@@ -8,38 +8,31 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
cleanup: cleanup:
name: Cleanup name: Cleanup
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
actions: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Cleanup - name: Cleanup
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ github.ref }}
run: | run: |
gh extension install actions/gh-actions-cache gh extension install actions/gh-actions-cache
REPO=${{ github.repository }} REPO=${{ github.repository }}
BRANCH=${{ github.ref }}
echo "Fetching list of cache keys" echo "Fetching list of cache keys"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B ${REF} -L 100 | cut -f 1 ) cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys. ## Setting this to not fail the workflow while deleting cache keys.
set +e set +e
echo "Deleting caches..." echo "Deleting caches..."
for cacheKey in $cacheKeysForPR for cacheKey in $cacheKeysForPR
do do
gh actions-cache delete $cacheKey -R "$REPO" -B "${REF}" --confirm gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done done
echo "Done" echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,6 +6,7 @@ on:
- 'cli/**' - 'cli/**'
- '.github/workflows/cli.yml' - '.github/workflows/cli.yml'
pull_request: pull_request:
branches: [main]
paths: paths:
- 'cli/**' - 'cli/**'
- '.github/workflows/cli.yml' - '.github/workflows/cli.yml'
@@ -16,41 +17,31 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {} permissions:
packages: write
jobs: jobs:
publish: publish:
name: CLI Publish name: Publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./cli
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - uses: actions/checkout@v4
with: # Setup .npmrc file to publish to npm
persist-credentials: false - uses: actions/setup-node@v4
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'pnpm' - name: Prepare SDK
cache-dependency-path: '**/pnpm-lock.yaml' run: npm ci --prefix ../open-api/typescript-sdk/
- name: Build SDK
- name: Setup typescript-sdk run: npm run build --prefix ../open-api/typescript-sdk/
run: pnpm install && pnpm run build - run: npm ci
working-directory: ./open-api/typescript-sdk - run: npm run build
- run: npm publish
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -58,25 +49,20 @@ jobs:
docker: docker:
name: Docker name: Docker
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: publish needs: publish
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@v3.5.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@v3
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
with: with:
registry: ghcr.io registry: ghcr.io
@@ -91,7 +77,7 @@ jobs:
- name: Generate docker image tags - name: Generate docker image tags
id: metadata id: metadata
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 uses: docker/metadata-action@v5
with: with:
flavor: | flavor: |
latest=false latest=false
@@ -102,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@v6.5.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -1,107 +0,0 @@
on:
issues:
types: [opened]
discussion:
types: [created]
name: Close likely duplicates
permissions: {}
jobs:
should_run:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.should_run.outputs.run }}
steps:
- id: should_run
run: echo "run=${{ github.event_name == 'issues' || github.event.discussion.category.name == 'Feature Request' }}" >> $GITHUB_OUTPUT
get_body:
runs-on: ubuntu-latest
needs: should_run
if: ${{ needs.should_run.outputs.should_run == 'true' }}
env:
EVENT: ${{ toJSON(github.event) }}
outputs:
body: ${{ steps.get_body.outputs.body }}
steps:
- id: get_body
run: |
BODY=$(echo """$EVENT""" | jq -r '.issue // .discussion | .body' | base64 -w 0)
echo "body=$BODY" >> $GITHUB_OUTPUT
get_checkbox_json:
runs-on: ubuntu-latest
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
image: ghcr.io/immich-app/mdq:main@sha256:d8ae47cf2e6cf4e2559bd57a60b73674fe44f897cba2c2bddff2987a05be10a4
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:
- id: get_checkbox
env:
BODY: ${{ needs.get_body.outputs.body }}
run: |
CHECKED=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes' | jq '.items[0].list[0].checked // false')
echo "checked=$CHECKED" >> $GITHUB_OUTPUT
close_and_comment:
runs-on: ubuntu-latest
needs: [get_checkbox_json, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' && needs.get_checkbox_json.outputs.checked != 'true' }}
permissions:
issues: write
discussions: write
steps:
- name: Close issue
if: ${{ github.event_name == 'issues' }}
env:
GH_TOKEN: ${{ github.token }}
NODE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql \
-f issueId="$NODE_ID" \
-f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \
-f query='
mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
addComment(input: {
subjectId: $issueId,
body: $body
}) {
__typename
}
closeIssue(input: {
issueId: $issueId,
stateReason: DUPLICATE
}) {
__typename
}
}'
- name: Close discussion
if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
env:
GH_TOKEN: ${{ github.token }}
NODE_ID: ${{ github.event.discussion.node_id }}
run: |
gh api graphql \
-f discussionId="$NODE_ID" \
-f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \
-f query='
mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
addDiscussionComment(input: {
discussionId: $discussionId,
body: $body
}) {
__typename
}
closeDiscussion(input: {
discussionId: $discussionId,
reason: DUPLICATE
}) {
__typename
}
}'

View File

@@ -9,14 +9,14 @@
# the `language` matrix defined below to confirm you have the correct set of # the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages. # supported CodeQL languages.
# #
name: 'CodeQL' name: "CodeQL"
on: on:
push: push:
branches: ['main'] branches: [ "main" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: ['main'] branches: [ "main" ]
schedule: schedule:
- cron: '20 13 * * 1' - cron: '20 13 * * 1'
@@ -24,8 +24,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@@ -38,44 +36,43 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ['javascript', 'python'] language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
# Command-line programs to run using the OS shell. # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# If the Autobuild fails above, remove it and uncomment the following three lines. # Command-line programs to run using the OS shell.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# - run: | # If the Autobuild fails above, remove it and uncomment the following three lines.
# echo "Run, Build Application using script" # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis # - run: |
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 # echo "Run, Build Application using script"
with: # ./location_of_script_within_repo/buildscript.sh
category: '/language:${{matrix.language}}'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

73
.github/workflows/docker-cleanup.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
# This workflow runs on certain conditions to check for and potentially
# delete container images from the GHCR which no longer have an associated
# code branch.
# Requires a PAT with the correct scope set in the secrets.
#
# This workflow will not trigger runs on forked repos.
name: Docker Cleanup
on:
pull_request:
types:
- "closed"
push:
paths:
- ".github/workflows/docker-cleanup.yml"
concurrency:
group: registry-tags-cleanup
cancel-in-progress: false
jobs:
cleanup-images:
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
is_org: "true"
do_delete: "true"
package_name: "${{ matrix.primary-name }}"
scheme: "pull_request"
repo_name: "immich"
match_regex: '^pr-(\d+)$|^(\d+)$'
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-22.04
needs:
- cleanup-images
strategy:
fail-fast: false
matrix:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
- 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
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.7.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
do_delete: "true"
is_org: "true"
package_name: "${{ matrix.primary-name }}"

View File

@@ -5,6 +5,7 @@ on:
push: push:
branches: [main] branches: [main]
pull_request: pull_request:
branches: [main]
release: release:
types: [published] types: [published]
@@ -12,175 +13,122 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {} permissions:
packages: write
jobs: jobs:
pre-job: build_and_push:
runs-on: ubuntu-latest name: Build and Push
permissions:
contents: read
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
server:
- 'server/**'
- 'openapi/**'
- 'web/**'
- 'i18n/**'
machine-learning:
- 'machine-learning/**'
force-filters: |
- '.github/workflows/docker.yml'
- '.github/workflows/multi-runner-build.yml'
- '.github/actions/image-build'
force-events: 'workflow_dispatch,release'
retag_ml:
name: Re-Tag ML
needs: pre-job
permissions:
contents: read
packages: write
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == false && !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: # Prevent a failure in one image from stopping the other builds
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Re-tag image
env:
REGISTRY_NAME: 'ghcr.io'
REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning
TAG_OLD: main${{ matrix.suffix }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
run: |
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
retag_server:
name: Re-Tag Server
needs: pre-job
permissions:
contents: read
packages: write
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest
strategy:
matrix:
suffix: ['']
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Re-tag image
env:
REGISTRY_NAME: 'ghcr.io'
REPOSITORY: ${{ github.repository_owner }}/immich-server
TAG_OLD: main${{ matrix.suffix }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
run: |
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
machine-learning:
name: Build and Push ML
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }}
strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- device: cpu - image: immich-machine-learning
tag-suffix: '' context: machine-learning
- device: cuda file: machine-learning/Dockerfile
tag-suffix: '-cuda' platforms: linux/amd64,linux/arm64
device: cpu
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
- device: openvino device: cuda
tag-suffix: '-openvino' suffix: -cuda
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
- device: armnn device: openvino
tag-suffix: '-armnn' suffix: -openvino
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/arm64 platforms: linux/arm64
- device: rknn device: armnn
tag-suffix: '-rknn' suffix: -armnn
platforms: linux/arm64
- device: rocm
tag-suffix: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
permissions:
contents: read
actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-machine-learning
context: machine-learning
dockerfile: machine-learning/Dockerfile
platforms: ${{ matrix.platforms }}
runner-mapping: ${{ matrix.runner-mapping }}
tag-suffix: ${{ matrix.tag-suffix }}
dockerhub-push: ${{ github.event_name == 'release' }}
build-args: |
DEVICE=${{ matrix.device }}
server: - image: immich-server
name: Build and Push Server context: .
needs: pre-job file: server/Dockerfile
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} platforms: linux/amd64,linux/arm64
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 device: cpu
permissions:
contents: read
actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-server
context: .
dockerfile: server/Dockerfile
dockerhub-push: ${{ github.event_name == 'release' }}
build-args: |
DEVICE=cpu
success-check-server:
name: Docker Build & Push Server Success
needs: [server, retag_server]
permissions: {}
runs-on: ubuntu-latest
if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 - name: Checkout
with: uses: actions/checkout@v4
needs: ${{ toJSON(needs) }}
success-check-ml: - name: Set up QEMU
name: Docker Build & Push ML Success uses: docker/setup-qemu-action@v3.2.0
needs: [machine-learning, retag_ml]
permissions: {} - name: Set up Docker Buildx
runs-on: ubuntu-latest uses: docker/setup-buildx-action@v3.5.0
if: always()
steps: - name: Login to Docker Hub
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 # Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with: with:
needs: ${{ toJSON(needs) }} 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,suffix=${{ matrix.suffix }}
# Tag with pr-number
type=ref,event=pr,suffix=${{ matrix.suffix }}
# Tag with git tag on release
type=ref,event=tag,suffix=${{ matrix.suffix }}
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
- 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@v6.5.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 }}
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-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}

View File

@@ -2,7 +2,12 @@ name: Docs build
on: on:
push: push:
branches: [main] branches: [main]
paths:
- "docs/**"
pull_request: pull_request:
branches: [main]
paths:
- "docs/**"
release: release:
types: [published] types: [published]
@@ -10,36 +15,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
docs:
- 'docs/**'
open-api:
- 'open-api/immich-openapi-specs.json'
force-filters: |
- '.github/workflows/docs-build.yml'
force-events: 'release'
force-branches: 'main'
build: build:
name: Docs Build
needs: pre-job
permissions:
contents: read
if: ${{ fromJSON(needs.pre-job.outputs.should_run).docs == true }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
@@ -47,33 +24,25 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './docs/.nvmrc' node-version-file: './docs/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run install - name: Run npm install
run: pnpm install run: npm ci
- name: Check formatting - name: Check formatting
run: pnpm format run: npm run format
- name: Run build - name: Run build
run: pnpm build run: npm run build
- name: Upload build output - name: Upload build output
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: docs-build-output name: docs-build-output
path: docs/build/ path: docs/build/
include-hidden-files: true
retention-days: 1 retention-days: 1

View File

@@ -1,46 +1,22 @@
name: Docs deploy name: Docs deploy
on: on:
workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here workflow_run:
workflows: ['Docs build'] workflows: ["Docs build"]
types: types:
- completed - completed
jobs: jobs:
checks: checks:
name: Docs Deploy Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
actions: read
pull-requests: read
outputs: outputs:
parameters: ${{ steps.parameters.outputs.result }} parameters: ${{ steps.parameters.outputs.result }}
artifact: ${{ steps.get-artifact.outputs.result }}
steps: steps:
- if: ${{ github.event.workflow_run.conclusion != 'success' }} - if: ${{ github.event.workflow_run.conclusion == 'failure' }}
run: echo 'The triggering workflow did not succeed' && exit 1 run: echo 'The triggering workflow failed' && exit 1
- name: Get artifact
id: get-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
if (!matchArtifact) {
console.log("No artifact found with the name docs-build-output, build job was skipped")
return { found: false };
}
return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters - name: Determine deploy parameters
id: parameters id: parameters
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with: with:
script: | script: |
const eventType = context.payload.workflow_run.event; const eventType = context.payload.workflow_run.event;
@@ -62,8 +38,7 @@ jobs:
} else if (eventType == "pull_request") { } else if (eventType == "pull_request") {
let pull_number = context.payload.workflow_run.pull_requests[0]?.number; let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
if(!pull_number) { if(!pull_number) {
const {HEAD_SHA} = process.env; const response = await github.rest.search.issuesAndPullRequests({q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',per_page: 1,})
const response = await github.rest.search.issuesAndPullRequests({q: `repo:${{ github.repository }} is:pr sha:${HEAD_SHA}`,per_page: 1,})
const items = response.data.items const items = response.data.items
if (items.length < 1) { if (items.length < 1) {
throw new Error("No pull request found for the commit") throw new Error("No pull request found for the commit")
@@ -98,43 +73,43 @@ jobs:
return parameters; return parameters;
deploy: deploy:
name: Docs Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: checks needs: checks
permissions: if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }}
contents: read
actions: read
pull-requests: write
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Load parameters - name: Load parameters
id: parameters id: parameters
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with: with:
script: | script: |
const parameters = JSON.parse(process.env.PARAM_JSON); const json = `${{ needs.checks.outputs.parameters }}`;
const parameters = JSON.parse(json);
core.setOutput("event", parameters.event); core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name); core.setOutput("name", parameters.name);
core.setOutput("shouldDeploy", parameters.shouldDeploy); core.setOutput("shouldDeploy", parameters.shouldDeploy);
- run: |
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
- name: Download artifact - name: Download artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@v7
env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with: with:
script: | script: |
let artifact = JSON.parse(process.env.ARTIFACT_JSON); let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
let download = await github.rest.actions.downloadArtifact({ let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
artifact_id: artifact.id, artifact_id: matchArtifact.id,
archive_format: 'zip', archive_format: 'zip',
}); });
let fs = require('fs'); let fs = require('fs');
@@ -150,12 +125,12 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@v2
with: with:
tg_version: '0.58.12' tg_version: "0.58.12"
tofu_version: '1.7.1' tofu_version: "1.7.1"
tg_dir: 'deployment/modules/cloudflare/docs' tg_dir: "deployment/modules/cloudflare/docs"
tg_command: 'apply' tg_command: "apply"
- name: Deploy Docs Subdomain Output - name: Deploy Docs Subdomain Output
id: docs-output id: docs-output
@@ -165,30 +140,27 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@v2
with: with:
tg_version: '0.58.12' tg_version: "0.58.12"
tofu_version: '1.7.1' tofu_version: "1.7.1"
tg_dir: 'deployment/modules/cloudflare/docs' tg_dir: "deployment/modules/cloudflare/docs"
tg_command: 'output -json' tg_command: "output -json"
- name: Output Cleaning - name: Output Cleaning
id: clean id: clean
env:
TG_OUTPUT: ${{ steps.docs-output.outputs.tg_action_output }}
run: | run: |
CLEANED=$(echo "$TG_OUTPUT" | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .) TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
echo "output=$CLEANED" >> $GITHUB_OUTPUT echo "output=$TG_OUT" >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages - name: Publish to Cloudflare Pages
# TODO: Action is deprecated uses: cloudflare/pages-action@v1
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1.5.0
with: with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }} projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }}
workingDirectory: 'docs' workingDirectory: "docs"
directory: 'build' directory: "build"
branch: ${{ steps.parameters.outputs.name }} branch: ${{ steps.parameters.outputs.name }}
wranglerVersion: '3' wranglerVersion: '3'
@@ -199,7 +171,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@v2
with: with:
tg_version: '0.58.12' tg_version: '0.58.12'
tofu_version: '1.7.1' tofu_version: '1.7.1'
@@ -207,7 +179,7 @@ jobs:
tg_command: 'apply' tg_command: 'apply'
- name: Comment - name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 uses: actions-cool/maintain-one-comment@v3
if: ${{ steps.parameters.outputs.event == 'pr' }} if: ${{ steps.parameters.outputs.event == 'pr' }}
with: with:
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }} number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}

View File

@@ -1,39 +1,31 @@
name: Docs destroy name: Docs destroy
on: on:
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here pull_request_target:
types: [closed] types: [closed]
permissions: {}
jobs: jobs:
deploy: deploy:
name: Docs Destroy
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Destroy Docs Subdomain - name: Destroy Docs Subdomain
env: env:
TF_VAR_prefix_name: 'pr-${{ github.event.number }}' TF_VAR_prefix_name: "pr-${{ github.event.number }}"
TF_VAR_prefix_event_type: 'pr' TF_VAR_prefix_event_type: "pr"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8 uses: gruntwork-io/terragrunt-action@v2
with: with:
tg_version: '0.58.12' tg_version: "0.58.12"
tofu_version: '1.7.1' tofu_version: "1.7.1"
tg_dir: 'deployment/modules/cloudflare/docs' tg_dir: "deployment/modules/cloudflare/docs"
tg_command: 'destroy -refresh=false' tg_command: "destroy"
- name: Comment - name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 uses: actions-cool/maintain-one-comment@v3
with: with:
number: ${{ github.event.number }} number: ${{ github.event.number }}
delete: true delete: true

View File

@@ -1,60 +0,0 @@
name: Fix formatting
on:
pull_request:
types: [labeled]
permissions: {}
jobs:
fix-formatting:
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'fix:formatting' }}
permissions:
contents: write
pull-requests: write
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: 'Checkout'
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Fix formatting
run: make install-all && make format-all
- name: Commit and push
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
with:
default_author: github_actions
message: 'chore: fix formatting'
- name: Remove label
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
if: always()
with:
script: |
github.rest.issues.removeLabel({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'fix:formatting'
})

View File

@@ -1,128 +0,0 @@
name: Merge translations
on:
workflow_dispatch:
workflow_call:
secrets:
PUSH_O_MATIC_APP_ID:
required: true
PUSH_O_MATIC_APP_KEY:
required: true
WEBLATE_TOKEN:
required: true
inputs:
skip:
description: 'Skip translations'
required: false
type: boolean
permissions: {}
env:
WEBLATE_HOST: 'https://hosted.weblate.org'
WEBLATE_COMPONENT: 'immich/immich'
jobs:
merge:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Find translation PR
id: find_pr
if: ${{ inputs.skip != true }}
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
PR=$(gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable)
echo "$PR"
PR_NUMBER=$(echo "$PR" | jq '
if length == 1 then
.[0].number
else
error("Expected exactly 1 entry, got \(length)")
end
' 2>&1) || exit 1
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "Selected PR $PR_NUMBER"
if ! echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"'; then
echo "PR is not mergeable"
exit 1
fi
- name: Generate a token
id: generate_token
if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Lock weblate
if: ${{ inputs.skip != true }}
env:
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
run: |
curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=true
- name: Commit translations
if: ${{ inputs.skip != true }}
env:
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
run: |
curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=commit
curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=push
- name: Merge PR
id: merge_pr
if: ${{ inputs.skip != true }}
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
run: |
set -euo pipefail
REVIEW_ID=$(gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \
| jq '.id')
echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash
- name: Wait for PR to merge
if: ${{ inputs.skip != true }}
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
REVIEW_ID: ${{ steps.merge_pr.outputs.REVIEW_ID }}
run: |
# So we clean up no matter what
set +e
for i in {1..100}; do
if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state | jq -e '.state == "MERGED"'; then
echo "PR merged"
exit 0
else
echo "PR not merged yet, waiting..."
sleep 6
fi
done
echo "PR did not merge in time"
gh api -X PUT "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews/$REVIEW_ID/dismissals" --field message='Merge attempt timed out' --field event='DISMISS'
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --disable-auto
exit 1
- name: Unlock weblate
if: ${{ inputs.skip != true }}
env:
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
run: |
curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=false
- name: Report success
run: |
echo "Workflow completed successfully (or was skipped)"

View File

@@ -1,12 +0,0 @@
name: PR Conventional Commit
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr-title:
name: Validate PR Title (conventional commit)
uses: immich-app/devtools/.github/workflows/shared-pr-require-conventional-commit.yml@main
permissions:
pull-requests: write

View File

@@ -1,15 +0,0 @@
name: Zizmor
on:
pull_request:
push:
branches: [main]
jobs:
zizmor:
name: Zizmor
uses: immich-app/devtools/.github/workflows/shared-zizmor.yml@main
permissions:
actions: read
contents: read
security-events: write

View File

@@ -1,24 +0,0 @@
name: PR Label Validation
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
types: [opened, labeled, unlabeled, synchronize]
permissions: {}
jobs:
validate-release-label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
with:
mode: exactly
count: 1
use_regex: true
labels: 'changelog:.*'
add_comment: true
message: 'Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label.'

View File

@@ -1,8 +1,6 @@
name: 'Pull Request Labeler' name: "Pull Request Labeler"
on: on:
- pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here - pull_request_target
permissions: {}
jobs: jobs:
labeler: labeler:
@@ -11,4 +9,4 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 - uses: actions/labeler@v5

View File

@@ -0,0 +1,15 @@
name: PR Conventional Commit Validation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr-title:
runs-on: ubuntu-latest
steps:
- name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@1.2.0
with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
add_label: 'false'

View File

@@ -10,129 +10,74 @@ on:
type: choice type: choice
options: options:
- 'false' - 'false'
- major
- minor - minor
- patch - patch
mobileBump: mobileBump:
description: 'Bump mobile build number' description: 'Bump mobile build number'
required: false required: false
type: boolean type: boolean
skipTranslations:
description: 'Skip translations'
required: false
type: boolean
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-root group: ${{ github.workflow }}-${{ github.ref }}-root
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
merge_translations:
uses: ./.github/workflows/merge-translations.yml
with:
skip: ${{ inputs.skipTranslations }}
permissions:
pull-requests: write
secrets:
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
bump_version: bump_version:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [merge_translations]
outputs: outputs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }} ref: ${{ steps.push-tag.outputs.commit_long_sha }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps: steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ secrets.ORG_RELEASE_TOKEN }}
persist-credentials: true
ref: main
- name: Install uv - name: Install Poetry
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 run: pipx install poetry
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Bump version - name: Bump version
env: run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
SERVER_BUMP: ${{ inputs.serverBump }}
MOBILE_BUMP: ${{ inputs.mobileBump }}
run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
- name: Commit and tag - name: Commit and tag
id: push-tag id: push-tag
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 uses: EndBug/add-and-commit@v9
with: with:
default_author: github_actions author_name: Alex The Bot
message: 'chore: version ${{ env.IMMICH_VERSION }}' author_email: alex.tran1502@gmail.com
default_author: user_info
message: 'Version ${{ env.IMMICH_VERSION }}'
tag: ${{ env.IMMICH_VERSION }} tag: ${{ env.IMMICH_VERSION }}
push: true push: true
build_mobile: build_mobile:
uses: ./.github/workflows/build-mobile.yml uses: ./.github/workflows/build-mobile.yml
needs: bump_version needs: bump_version
permissions: secrets: inherit
contents: read
secrets:
KEY_JKS: ${{ secrets.KEY_JKS }}
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
with: with:
ref: ${{ needs.bump_version.outputs.ref }} ref: ${{ needs.bump_version.outputs.ref }}
prepare_release: prepare_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build_mobile needs: build_mobile
permissions:
actions: read # To download the app artifact
# No content permissions are needed because it uses the app-token
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ secrets.ORG_RELEASE_TOKEN }}
persist-credentials: false
- name: Download APK - name: Download APK
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@v4
with: with:
name: release-apk-signed name: release-apk-signed
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@v2
with: with:
draft: true draft: true
tag_name: ${{ env.IMMICH_VERSION }} tag_name: ${{ env.IMMICH_VERSION }}
token: ${{ steps.generate-token.outputs.token }}
generate_release_notes: true generate_release_notes: true
body_path: misc/release/notes.tmpl body_path: misc/release/notes.tmpl
files: | files: |

View File

@@ -1,47 +0,0 @@
name: Preview label
on:
pull_request:
types: [labeled, closed]
permissions: {}
jobs:
comment-status:
runs-on: ubuntu-latest
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'preview' }}
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
with:
message-id: 'preview-status'
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/'
remove-label:
runs-on: ubuntu-latest
if: ${{ (github.event.action == 'closed' || github.event.pull_request.head.repo.fork) && contains(github.event.pull_request.labels.*.name, 'preview') }}
permissions:
pull-requests: write
steps:
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
github.rest.issues.removeLabel({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'preview'
})
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
if: ${{ github.event.pull_request.head.repo.fork }}
with:
message-id: 'preview-status'
message: 'PRs from forks cannot have preview environments.'
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
message-id: 'preview-status'
message: 'Preview environment has been removed.'

View File

@@ -4,37 +4,28 @@ on:
release: release:
types: [published] types: [published]
permissions: {} permissions:
packages: write
jobs: jobs:
publish: publish:
name: Publish `@immich/sdk` name: Publish `@immich/sdk`
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@v4
with: with:
node-version-file: './open-api/typescript-sdk/.nvmrc' node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install deps - name: Install deps
run: pnpm install --frozen-lockfile run: npm ci
- name: Build - name: Build
run: pnpm build run: npm run build
- name: Publish - name: Publish
run: pnpm publish run: npm publish
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -9,96 +9,35 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs: jobs:
pre-job:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
mobile:
- 'mobile/**'
force-filters: |
- '.github/workflows/static_analysis.yml'
force-events: 'workflow_dispatch,release'
mobile-dart-analyze: mobile-dart-analyze:
name: Run Dart Code Analysis name: Run Dart Code Analysis
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./mobile
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get
working-directory: ./mobile
- name: Install DCM
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
version: auto
working-directory: ./mobile
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
- name: Run Build Runner
run: make build
- name: Generate platform API
run: make pigeon
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
mobile/**/*.g.dart
mobile/**/*.gr.dart
mobile/**/*.drift.dart
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: |
echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory"
echo "Changed files: ${CHANGED_FILES}"
exit 1
- name: Run dart analyze - name: Run dart analyze
run: dart analyze --fatal-infos run: dart analyze --fatal-infos
working-directory: ./mobile
- name: Run dart format - name: Run dart format
run: make format run: dart format lib/ --set-exit-if-changed
working-directory: ./mobile
# TODO: Re-enable after upgrading custom_lint # Enable after riverpod generator migration is completed
# - name: Run dart custom_lint # - name: Run dart custom lint
# run: dart run custom_lint # run: dart run custom_lint
# working-directory: ./mobile
# TODO: Use https://github.com/CQLabs/dcm-action
- name: Run DCM
run: dcm analyze lib --fatal-style --fatal-warnings

View File

@@ -4,725 +4,464 @@ on:
pull_request: pull_request:
push: push:
branches: [main] branches: [main]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {}
jobs:
pre-job:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
i18n:
- 'i18n/**'
web:
- 'web/**'
- 'i18n/**'
- 'open-api/typescript-sdk/**'
server:
- 'server/**'
cli:
- 'cli/**'
- 'open-api/typescript-sdk/**'
e2e:
- 'e2e/**'
mobile:
- 'mobile/**'
machine-learning:
- 'machine-learning/**'
.github:
- '.github/**'
force-filters: |
- '.github/workflows/test.yml'
force-events: 'workflow_dispatch'
jobs:
server-unit-tests: server-unit-tests:
name: Test & Lint Server name: Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./server working-directory: ./server
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' - name: Run npm install
- name: Run package manager install run: npm ci
run: pnpm install
- name: Run linter - name: Run linter
run: pnpm lint run: npm run lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: pnpm format run: npm run format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: pnpm check run: npm run check
if: ${{ !cancelled() }}
- name: Run small tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
cli-unit-tests:
name: Unit Test CLI
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).cli == true }}
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: pnpm test run: npm run test:cov
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
cli-unit-tests-win:
name: Unit Test CLI (Windows) cli-unit-tests:
needs: pre-job name: CLI
if: ${{ fromJSON(needs.pre-job.outputs.should_run).cli == true }} runs-on: ubuntu-latest
runs-on: windows-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./cli working-directory: ./cli
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk - name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Install deps
run: pnpm install --frozen-lockfile run: npm ci
- name: Run linter
run: npm run lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test:cov
if: ${{ !cancelled() }}
cli-unit-tests-win:
name: CLI (Windows)
runs-on: windows-latest
defaults:
run:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './cli/.nvmrc'
- name: Setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: npm ci
# Skip linter & formatter in Windows test. # Skip linter & formatter in Windows test.
- name: Run tsc - name: Run tsc
run: pnpm check run: npm run check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: pnpm test run: npm run test:cov
if: ${{ !cancelled() }}
web-lint:
name: Lint Web
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).web == true }}
runs-on: mich
permissions:
contents: read
defaults:
run:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run pnpm install
run: pnpm rebuild && pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint:p
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run svelte checks
run: pnpm check:svelte
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-unit-tests: web-unit-tests:
name: Test Web name: Web
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).web == true }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./web working-directory: ./web
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Run npm install - name: Run npm install
run: pnpm install --frozen-lockfile run: npm ci
- name: Run tsc
run: pnpm check:typescript
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
i18n-tests:
name: Test i18n
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install dependencies
run: pnpm --filter=immich-web install --frozen-lockfile
- name: Format
run: pnpm --filter=immich-web format:i18n
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
i18n/**
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: |
echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
e2e-tests-lint:
name: End-to-End Lint
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true }}
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./e2e
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Run linter - name: Run linter
run: pnpm lint run: npm run lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: pnpm format run: npm run format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run svelte checks
run: npm run check:svelte
if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: pnpm check run: npm run check:typescript
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
server-medium-tests:
name: Medium Tests (Server) - name: Run unit tests & coverage
needs: pre-job run: npm run test:cov
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} if: ${{ !cancelled() }}
e2e-tests:
name: End-to-End Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./server
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run pnpm install
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Run medium tests
run: pnpm test:medium
if: ${{ !cancelled() }}
e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI)
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./e2e working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with: with:
persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- name: Run setup cli - name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build run: npm ci && npm run build
working-directory: ./cli working-directory: ./cli
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: npm ci
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Docker build
run: docker compose build - name: Run linter
run: npm run lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
run: pnpm test - name: Run formatter
run: npm run format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-web:
name: End-to-End Tests (Web) - name: Run tsc
needs: pre-job run: npm run check
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
defaults:
run:
working-directory: ./e2e
strategy:
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: npx playwright install chromium --only-shell run: npx playwright install --with-deps chromium
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Docker build - name: Docker build
run: docker compose build run: docker compose build
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
run: npm run test
if: ${{ !cancelled() }}
- name: Run e2e tests (web) - name: Run e2e tests (web)
run: npx playwright test run: npx playwright test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
permissions: {}
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
with:
needs: ${{ toJSON(needs) }}
mobile-unit-tests: mobile-unit-tests:
name: Unit Test Mobile name: Mobile
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
working-directory: ./mobile
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
ml-unit-tests: ml-unit-tests:
name: Unit Test ML name: Machine Learning
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
defaults: defaults:
run: run:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v5
with: with:
persist-credentials: false python-version: 3.11
- name: Install uv cache: 'poetry'
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
# with:
# python-version: 3.11
# cache: 'uv'
- name: Install dependencies - name: Install dependencies
run: | run: |
uv sync --extra cpu poetry install --with dev --with cpu
- name: Lint with ruff - name: Lint with ruff
run: | run: |
uv run ruff check --output-format=github immich_ml poetry run ruff check --output-format=github app export
- name: Check black formatting - name: Check black formatting
run: | run: |
uv run black --check immich_ml poetry run black --check app export
- name: Run mypy type checking - name: Run mypy type checking
run: | run: |
uv run mypy --strict immich_ml/ poetry run mypy --install-types --non-interactive --strict app/
- name: Run tests and coverage - name: Run tests and coverage
run: | run: |
uv run pytest --cov=immich_ml --cov-report term-missing poetry run pytest app --cov=app --cov-report term-missing
github-files-formatting:
name: .github Files Formatting
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run)['.github'] == true }}
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./.github
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './.github/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run pnpm install
run: pnpm install --frozen-lockfile
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
shellcheck: shellcheck:
name: ShellCheck name: ShellCheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Run ShellCheck - name: Run ShellCheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 uses: ludeeus/action-shellcheck@master
with: with:
ignore_paths: >- ignore_paths: >-
**/open-api/** **/openapi** **/node_modules/** **/open-api/**
**/openapi/**
**/node_modules/**
generated-api-up-to-date: generated-api-up-to-date:
name: OpenAPI Clients name: OpenAPI Clients
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install server dependencies - name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile run: npm --prefix=server ci
- name: Build the app - name: Build the app
run: pnpm --filter immich build run: npm --prefix=server run build
- name: Run API generation - name: Run API generation
run: ./bin/generate-open-api.sh run: make open-api
working-directory: open-api
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@v20
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
mobile/openapi mobile/openapi
open-api/typescript-sdk open-api/typescript-sdk
open-api/immich-openapi-specs.json open-api/immich-openapi-specs.json
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated files not up to date!" echo "ERROR: Generated files not up to date!"
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
exit 1 exit 1
sql-schema-up-to-date:
name: SQL Schema Checks generated-typeorm-migrations-up-to-date:
name: TypeORM Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
services: services:
postgres: postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:da52bbead5d818adaa8077c8dcdaad0aaf93038c31ad8348b51f9f0ec1310a4d image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_DB: immich POSTGRES_DB: immich
options: >- options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports: ports:
- 5432:5432 - 5432:5432
defaults: defaults:
run: run:
working-directory: ./server working-directory: ./server
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install server dependencies - name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile run: npm ci
- name: Build the app - name: Build the app
run: pnpm build run: npm run build
- name: Run existing migrations - name: Run existing migrations
run: pnpm migrations:run run: npm run typeorm:migrations:run
- name: Test npm run schema:reset command works - name: Test npm run schema:reset command works
run: pnpm schema:reset run: npm run typeorm:schema:reset
- name: Generate new migrations - name: Generate new migrations
continue-on-error: true continue-on-error: true
run: pnpm migrations:generate src/TestMigration run: npm run typeorm:migrations:generate ./src/migrations/TestMigration
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@v20
id: verify-changed-files id: verify-changed-files
with: with:
files: | files: |
server/src server/src/migrations/
- name: Verify migration files have not changed - name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated migration files not up to date!" echo "ERROR: Generated migration files not up to date!"
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
cat ./src/*-TestMigration.ts
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: pnpm sync:sql run: npm run sync:sql
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@v20
id: verify-changed-sql-files id: verify-changed-sql-files
with: with:
files: | files: |
server/src/queries server/src/queries
- name: Verify SQL files have not changed - name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true' if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-sql-files.outputs.changed_files }}
run: | run: |
echo "ERROR: Generated SQL files not up to date!" echo "ERROR: Generated SQL files not up to date!"
echo "Changed files: ${CHANGED_FILES}" echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}"
git diff
exit 1 exit 1
# mobile-integration-tests: # mobile-integration-tests:
# name: Run mobile end-to-end integration tests # name: Run mobile end-to-end integration tests
# runs-on: macos-latest # runs-on: macos-latest
# steps: # steps:
# - uses: actions/checkout@v4 # - uses: actions/checkout@v4
# - uses: actions/setup-java@v3 # - uses: actions/setup-java@v3
# with: # with:
# distribution: 'zulu' # distribution: 'zulu'
# java-version: '12.x' # java-version: '12.x'
# cache: 'gradle' # cache: 'gradle'
# - name: Cache android SDK # - name: Cache android SDK
# uses: actions/cache@v3 # uses: actions/cache@v3
# id: android-sdk # id: android-sdk
# with: # with:
# key: android-sdk # key: android-sdk
# path: | # path: |
# /usr/local/lib/android/ # /usr/local/lib/android/
# ~/.android # ~/.android
# - name: Cache Gradle # - name: Cache Gradle
# uses: actions/cache@v3 # uses: actions/cache@v3
# with: # with:
# path: | # path: |
# ./mobile/build/ # ./mobile/build/
# ./mobile/android/.gradle/ # ./mobile/android/.gradle/
# key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }} # key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }}
# - name: Setup Android SDK # - name: Setup Android SDK
# if: steps.android-sdk.outputs.cache-hit != 'true' # if: steps.android-sdk.outputs.cache-hit != 'true'
# uses: android-actions/setup-android@v2 # uses: android-actions/setup-android@v2
# - name: AVD cache # - name: AVD cache
# uses: actions/cache@v3 # uses: actions/cache@v3
# id: avd-cache # id: avd-cache
# with: # with:
# path: | # path: |
# ~/.android/avd/* # ~/.android/avd/*
# ~/.android/adb* # ~/.android/adb*
# key: avd-29 # key: avd-29
# - name: create AVD and generate snapshot for caching # - name: create AVD and generate snapshot for caching
# if: steps.avd-cache.outputs.cache-hit != 'true' # if: steps.avd-cache.outputs.cache-hit != 'true'
# uses: reactivecircus/android-emulator-runner@v2.27.0 # uses: reactivecircus/android-emulator-runner@v2.27.0
# with: # with:
# working-directory: ./mobile # working-directory: ./mobile
# cores: 2 # cores: 2
# api-level: 29 # api-level: 29
# arch: x86_64 # arch: x86_64
# profile: pixel # profile: pixel
# target: default # target: default
# force-avd-creation: false # force-avd-creation: false
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none # emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: false # disable-animations: false
# script: echo "Generated AVD snapshot for caching." # script: echo "Generated AVD snapshot for caching."
# - name: Setup Flutter SDK # - name: Setup Flutter SDK
# uses: subosito/flutter-action@v2 # uses: subosito/flutter-action@v2
# with: # with:
# channel: 'stable' # channel: 'stable'
# flutter-version: '3.7.3' # flutter-version: '3.7.3'
# cache: true # cache: true
# - name: Run integration tests # - name: Run integration tests
# uses: Wandalen/wretry.action@master # uses: Wandalen/wretry.action@master
# with: # with:
# action: reactivecircus/android-emulator-runner@v2.27.0 # action: reactivecircus/android-emulator-runner@v2.27.0
# with: | # with: |
# working-directory: ./mobile # working-directory: ./mobile
# cores: 2 # cores: 2
# api-level: 29 # api-level: 29
# arch: x86_64 # arch: x86_64
# profile: pixel # profile: pixel
# target: default # target: default
# force-avd-creation: false # force-avd-creation: false
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none # emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: true # disable-animations: true
# script: | # script: |
# flutter pub get # flutter pub get
# flutter test integration_test # flutter test integration_test
# attempt_limit: 3 # attempt_limit: 3

View File

@@ -1,61 +0,0 @@
name: Weblate checks
on:
pull_request:
branches: [main]
types:
- opened
- synchronize
- ready_for_review
- auto_merge_enabled
- auto_merge_disabled
permissions: {}
env:
BOT_NAME: immich-push-o-matic
jobs:
pre-job:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with:
filters: |
i18n:
- 'i18n/!(en)**\.json'
exclude-branches: 'chore/translations'
skip-force-logic: 'true'
enforce-lock:
name: Check Weblate Lock
needs: [pre-job]
runs-on: ubuntu-latest
permissions: {}
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps:
- name: Bot review status
env:
PR_NUMBER: ${{ github.event.pull_request.number || github.event.pull_request_review.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
# Then check for APPROVED by the bot, if absent fail
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == env.BOT_NAME and .state == "APPROVED")) | length > 0' \
|| (echo "The push-o-matic bot has not approved this PR yet" && exit 1)
success-check-lock:
name: Weblate Lock Check Success
needs: [enforce-lock]
runs-on: ubuntu-latest
permissions: {}
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
with:
needs: ${{ toJSON(needs) }}

7
.gitignore vendored
View File

@@ -3,7 +3,6 @@
.DS_Store .DS_Store
.vscode/* .vscode/*
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json
.idea .idea
docker/upload docker/upload
@@ -18,13 +17,7 @@ mobile/libisar.dylib
mobile/openapi/test mobile/openapi/test
mobile/openapi/doc mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES mobile/openapi/.openapi-generator/FILES
mobile/ios/build
open-api/typescript-sdk/build open-api/typescript-sdk/build
mobile/android/fastlane/report.xml mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml mobile/ios/fastlane/report.xml
vite.config.js.timestamp-*
.pnpm-store
.devcontainer/library
.devcontainer/.env*

2
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "mobile/.isar"] [submodule "mobile/.isar"]
path = mobile/.isar path = mobile/.isar
url = https://github.com/isar/isar url = https://github.com/isar/isar
[submodule "e2e/test-assets"] [submodule "server/test/assets"]
path = e2e/test-assets path = e2e/test-assets
url = https://github.com/immich-app/test-assets url = https://github.com/immich-app/test-assets

View File

@@ -1,18 +0,0 @@
module.exports = {
hooks: {
readPackage: (pkg) => {
if (!pkg.name) {
return pkg;
}
if (pkg.name === "exiftool-vendored") {
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
// make exiftool-vendored.pl a regular dependency
pkg.dependencies["exiftool-vendored.pl"] =
pkg.optionalDependencies["exiftool-vendored.pl"];
delete pkg.optionalDependencies["exiftool-vendored.pl"];
}
}
return pkg;
},
},
};

View File

@@ -1,10 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"svelte.svelte-vscode",
"dbaeumer.vscode-eslint",
"dart-code.flutter",
"dart-code.dart-code",
"dcmdev.dcm-vscode-extension"
]
}

12
.vscode/launch.json vendored
View File

@@ -5,18 +5,18 @@
"type": "node", "type": "node",
"request": "attach", "request": "attach",
"restart": true, "restart": true,
"port": 9231, "port": 9230,
"name": "Immich API Server", "name": "Immich Server",
"remoteRoot": "/usr/src/app/server", "remoteRoot": "/usr/src/app",
"localRoot": "${workspaceFolder}/server" "localRoot": "${workspaceFolder}/server"
}, },
{ {
"type": "node", "type": "node",
"request": "attach", "request": "attach",
"restart": true, "restart": true,
"port": 9230, "port": 9231,
"name": "Immich Workers", "name": "Immich Microservices",
"remoteRoot": "/usr/src/app/server", "remoteRoot": "/usr/src/app",
"localRoot": "${workspaceFolder}/server" "localRoot": "${workspaceFolder}/server"
} }
] ]

78
.vscode/settings.json vendored
View File

@@ -1,64 +1,44 @@
{ {
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.tabSize": 2,
"editor.formatOnSave": true
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.tabSize": 2 "editor.tabSize": 2
}, },
"svelte.enable-ts-plugin": true,
"eslint.validate": [
"javascript",
"svelte"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"[dart]": { "[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.selectionHighlight": false, "editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first", "editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets", "editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off" "editor.wordBasedSuggestions": "off",
"editor.defaultFormatter": "Dart-Code.dart-code"
}, },
"[javascript]": { "cSpell.words": [
"editor.codeActionsOnSave": { "immich"
"source.organizeImports": "explicit", ],
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[svelte]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"cSpell.words": ["immich"],
"editor.formatOnSave": true,
"eslint.validate": ["javascript", "svelte"],
"explorer.fileNesting.enabled": true, "explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart", "*.ts": "${capture}.spec.ts,${capture}.mock.ts"
"*.ts": "${capture}.spec.ts,${capture}.mock.ts", }
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
},
"svelte.enable-ts-plugin": true,
"typescript.preferences.importModuleSpecifier": "non-relative"
} }

72
.vscode/tasks.json vendored
View File

@@ -1,72 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Fix Permissions, Install Dependencies",
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{
"label": "Immich API Server (Nest)",
"dependsOn": ["Fix Permissions, Install Dependencies"],
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{
"label": "Immich Web Server (Vite)",
"dependsOn": ["Fix Permissions, Install Dependencies"],
"type": "shell",
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false,
"group": "Devcontainer tasks",
"close": true
},
"runOptions": {
"runOn": "default"
},
"problemMatcher": []
},
{
"label": "Immich Server and Web",
"dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"],
"runOptions": {
"runOn": "folderOpen"
},
"problemMatcher": []
}
]
}

View File

@@ -1,7 +1,5 @@
/.github/ @bo0tzz /.github/ @bo0tzz
/docker/ @bo0tzz /docker/ @bo0tzz
/server/ @danieldietzler /server/ @danieldietzler
/web/ @danieldietzler
/machine-learning/ @mertalev /machine-learning/ @mertalev
/e2e/ @danieldietzler /e2e/ @danieldietzler
/mobile/ @shenlong-tanwen

132
Makefile
View File

@@ -1,36 +1,24 @@
dev: dev:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
dev-down: dev-down:
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
dev-update: dev-update:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
dev-scale: dev-scale:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true 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
dev-docs:
npm --prefix docs run start
.PHONY: e2e .PHONY: e2e
e2e: e2e:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-update:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-down:
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
prod: prod:
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true 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-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale: prod-scale:
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true 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
.PHONY: open-api .PHONY: open-api
open-api: open-api:
@@ -43,7 +31,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql: sql:
pnpm --filter immich run sync:sql npm --prefix server run sync:sql
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh
@@ -51,101 +39,47 @@ attach-server:
renovate: renovate:
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
# Directories that need to be created for volumes or build output MODULES = e2e server web cli sdk
VOLUME_DIRS = \
./.pnpm-store \
./web/.svelte-kit \
./web/node_modules \
./web/coverage \
./e2e/node_modules \
./docs/node_modules \
./server/node_modules \
./open-api/typescript-sdk/node_modules \
./.github/node_modules \
./node_modules \
./cli/node_modules
# Include .env file if it exists
-include docker/.env
MODULES = e2e server web cli sdk docs .github
# directory to package name mapping function
# cli = @immich/cli
# docs = documentation
# e2e = immich-e2e
# open-api/typescript-sdk = @immich/sdk
# server = immich
# web = immich-web
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
audit-%: audit-%:
pnpm --filter $(call map-package,$*) audit fix npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
install-%: install-%:
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline) npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
build-cli: build-sdk build-cli: build-sdk
build-web: build-sdk build-web: build-sdk
build-%: install-% build-%: install-%
pnpm --filter $(call map-package,$*) run build npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'build' >/dev/null \
&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build || true
format-%: format-%:
pnpm --filter $(call map-package,$*) run format:fix npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'format:fix' >/dev/null \
&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run format:fix || true
lint-%: lint-%:
pnpm --filter $(call map-package,$*) run lint:fix npm --prefix $* run lint:fix
lint-web:
pnpm --filter $(call map-package,$*) run lint:p
check-%: check-%:
pnpm --filter $(call map-package,$*) run check npm --prefix $* run check
check-web: check-web:
pnpm --filter immich-web run check:typescript npm --prefix web run check:typescript
pnpm --filter immich-web run check:svelte npm --prefix web run check:svelte
test-%: test-%:
pnpm --filter $(call map-package,$*) run test npm --prefix $* run test
test-e2e: test-e2e:
docker compose -f ./e2e/docker-compose.yml build docker compose -f ./e2e/docker-compose.yml build
pnpm --filter immich-e2e run test npm --prefix e2e run test
pnpm --filter immich-e2e run test:web npm --prefix e2e run test:web
test-medium:
docker run \
--rm \
-v ./server/src:/usr/src/app/src \
-v ./server/test:/usr/src/app/test \
-v ./server/vitest.config.medium.mjs:/usr/src/app/vitest.config.medium.mjs \
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
-e NODE_ENV=development \
immich-server:latest \
-c "pnpm test:medium -- --run"
test-medium-dev:
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
install-all: build-all: $(foreach M,$(MODULES),build-$M) ;
pnpm -r --filter '!documentation' install install-all: $(foreach M,$(MODULES),install-$M) ;
check-all: $(foreach M,$(MODULES),check-$M) ;
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ; lint-all: $(foreach M,$(MODULES),lint-$M) ;
format-all: $(foreach M,$(MODULES),format-$M) ;
check-all: audit-all: $(foreach M,$(MODULES),audit-$M) ;
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/" hygiene-all: lint-all format-all check-all sql audit-all;
lint-all: test-all: $(foreach M,$(MODULES),test-$M) ;
pnpm -r --filter '!documentation' run lint:fix
format-all:
pnpm -r --filter '!documentation' run format:fix
audit-all:
pnpm -r --filter '!documentation' audit fix
hygiene-all: audit-all
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
test-all:
pnpm -r --filter '!documentation' run "/^test/"
clean: clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} + find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name "dist" -type d -prune -exec rm -rf '{}' + find . -name "dist" -type d -prune -exec rm -rf '{}' +
find . -name "build" -type d -prune -exec rm -rf '{}' + find . -name "build" -type d -prune -exec rm -rf '{}' +
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' + find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
find . -name "coverage" -type d -prune -exec rm -rf '{}' + docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' + docker compose -f ./e2e/docker-compose.yml rm -v -f || true
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
setup-server-dev: install-server
setup-web-dev: install-sdk build-sdk install-web

View File

@@ -1,11 +1,11 @@
<p align="center"> <p align="center">
<br/> <br/>
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a> <a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
<a href="https://discord.immich.app"> <a href="https://discord.immich.app">
<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" alt="Discord"/>
</a> </a>
<br/> <br/>
<br/> <br/>
</p> </p>
<p align="center"> <p align="center">
@@ -17,25 +17,23 @@
<img src="design/immich-screenshots.png" title="Main Screenshot"> <img src="design/immich-screenshots.png" title="Main Screenshot">
</a> </a>
<br/> <br/>
<p align="center"> <p align="center">
<a href="readme_i18n/README_ca_ES.md">Català</a>
<a href="readme_i18n/README_es_ES.md">Español</a> <a href="readme_i18n/README_ca_ES.md">Català</a>
<a href="readme_i18n/README_fr_FR.md">Français</a> <a href="readme_i18n/README_es_ES.md">Español</a>
<a href="readme_i18n/README_it_IT.md">Italiano</a> <a href="readme_i18n/README_fr_FR.md">Français</a>
<a href="readme_i18n/README_ja_JP.md">日本語</a> <a href="readme_i18n/README_it_IT.md">Italiano</a>
<a href="readme_i18n/README_ko_KR.md">한국어</a> <a href="readme_i18n/README_ja_JP.md">日本語</a>
<a href="readme_i18n/README_de_DE.md">Deutsch</a> <a href="readme_i18n/README_ko_KR.md">한국어</a>
<a href="readme_i18n/README_nl_NL.md">Nederlands</a> <a href="readme_i18n/README_de_DE.md">Deutsch</a>
<a href="readme_i18n/README_tr_TR.md">Türkçe</a> <a href="readme_i18n/README_nl_NL.md">Nederlands</a>
<a href="readme_i18n/README_zh_CN.md">中文</a> <a href="readme_i18n/README_tr_TR.md">Türkçe</a>
<a href="readme_i18n/README_uk_UA.md">Українська</a> <a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a> <a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a> <a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a> <a href="readme_i18n/README_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a> <a href="readme_i18n/README_ar_JO.md">العربية</a>
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
</p> </p>
## Disclaimer ## Disclaimer
@@ -61,7 +59,9 @@
## Demo ## Demo
Access the demo [here](https://demo.immich.app). For the mobile app, you can use `https://demo.immich.app` for the `Server Endpoint URL`. Access the demo [here](https://demo.immich.app). The demo is running on a Free-tier Oracle VM in Amsterdam with a 2.4Ghz quad-core ARM64 CPU and 24GB RAM.
For the mobile app, you can use `https://demo.immich.app/api` for the `Server Endpoint URL`
### Login credentials ### Login credentials
@@ -92,7 +92,7 @@ Access the demo [here](https://demo.immich.app). For the mobile app, you can use
| LivePhoto/MotionPhoto backup and playback | Yes | Yes | | LivePhoto/MotionPhoto backup and playback | Yes | Yes |
| Support 360 degree image display | No | Yes | | Support 360 degree image display | No | Yes |
| User-defined storage structure | Yes | Yes | | User-defined storage structure | Yes | Yes |
| Public Sharing | Yes | Yes | | Public Sharing | No | Yes |
| Archive and Favorites | Yes | Yes | | Archive and Favorites | Yes | Yes |
| Global Map | Yes | Yes | | Global Map | Yes | Yes |
| Partner Sharing | Yes | Yes | | Partner Sharing | Yes | Yes |
@@ -101,8 +101,6 @@ Access the demo [here](https://demo.immich.app). For the mobile app, you can use
| Offline support | Yes | No | | Offline support | Yes | No |
| Read-only gallery | Yes | Yes | | Read-only gallery | Yes | Yes |
| Stacked Photos | Yes | Yes | | Stacked Photos | Yes | Yes |
| Tags | No | Yes |
| Folder View | Yes | Yes |
## Translations ## Translations

1
cli/.eslintignore Normal file
View File

@@ -0,0 +1 @@
/dist

28
cli/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,28 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
root: true,
env: {
node: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/no-process-exit': 'off',
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
},
};

View File

@@ -1 +1 @@
22.19.0 20.16.0

View File

@@ -1,14 +1,19 @@
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core FROM node:20.16.0-alpine3.20@sha256:aada767bf3e4b4a1437642b81db7d8bb99a6dba27627088e4608772f1f02ebc0 as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
RUN npm ci
COPY open-api/typescript-sdk/ ./
RUN npm run build
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package* pnpm* .pnpmfile.cjs ./
COPY ./cli ./cli/ COPY cli/package.json cli/package-lock.json ./
COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/ RUN npm ci
RUN corepack enable pnpm && \
pnpm install --filter @immich/sdk --filter @immich/cli --frozen-lockfile && \ COPY cli .
pnpm --filter @immich/sdk build && \ RUN npm run build
pnpm --filter @immich/cli build
WORKDIR /import WORKDIR /import
ENTRYPOINT ["node", "/usr/src/app/cli/dist"] ENTRYPOINT ["node", "/usr/src/app/dist"]

View File

@@ -4,18 +4,8 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
# For developers # For developers
Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
$ npm install
$ npm run build
Then, to build the open-api client run the following in the open-api folder:
$ ./bin/generate-open-api.sh
To run the Immich CLI from source, run the following in the cli folder: To run the Immich CLI from source, run the following in the cli folder:
$ npm install
$ npm run build $ npm run build
$ ts-node . $ ts-node .
@@ -27,4 +17,3 @@ You can also build and install the CLI using
$ npm run build $ npm run build
$ npm install -g . $ npm install -g .
****

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env node
import '../dist/index.js';

View File

@@ -1,51 +0,0 @@
import js from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import typescriptEslint from 'typescript-eslint';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default typescriptEslint.config([
eslintPluginUnicorn.configs.recommended,
eslintPluginPrettierRecommended,
js.configs.recommended,
typescriptEslint.configs.recommended,
{
ignores: ['eslint.config.mjs', 'dist'],
},
{
languageOptions: {
globals: {
...globals.node,
},
parser: typescriptEslint.parser,
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
},
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/no-process-exit': 'off',
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
'object-shorthand': ['error', 'always'],
},
},
]);

4615
cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.92", "version": "2.2.12",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
"bin": { "bin": {
"immich": "./bin/immich" "immich": "dist/index.js"
}, },
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"keywords": [ "keywords": [
@@ -13,32 +13,30 @@
"cli" "cli"
], ],
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.8.0",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@types/byte-size": "^8.1.0", "@types/byte-size": "^8.1.0",
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^22.18.1", "@types/node": "^20.14.12",
"@vitest/coverage-v8": "^3.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"eslint": "^9.14.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^60.0.0", "eslint-plugin-unicorn": "^54.0.0",
"globals": "^16.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "vite": "^5.0.12",
"vite": "^7.0.0", "vite-tsconfig-paths": "^4.3.2",
"vite-tsconfig-paths": "^5.0.0", "vitest": "^1.2.2",
"vitest": "^3.0.0", "vitest-fetch-mock": "^0.2.2",
"vitest-fetch-mock": "^0.4.0",
"yaml": "^2.3.1" "yaml": "^2.3.1"
}, },
"scripts": { "scripts": {
@@ -61,13 +59,11 @@
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"chokidar": "^4.0.3",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fastq": "^1.17.1", "fastq": "^1.17.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21"
"micromatch": "^4.0.8"
}, },
"volta": { "volta": {
"node": "22.19.0" "node": "20.16.0"
} }
} }

View File

@@ -1,13 +1,12 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as os from 'node:os'; import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises'; import { describe, expect, it, vi } from 'vitest';
import { describe, expect, it, MockedFunction, vi } from 'vitest';
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk';
import createFetchMock from 'vitest-fetch-mock'; import createFetchMock from 'vitest-fetch-mock';
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset'; import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset';
vi.mock('@immich/sdk'); vi.mock('@immich/sdk');
@@ -200,112 +199,3 @@ describe('checkForDuplicates', () => {
}); });
}); });
}); });
describe('startWatch', () => {
let testFolder: string;
let checkBulkUploadMocked: MockedFunction<typeof checkBulkUpload>;
beforeEach(async () => {
vi.restoreAllMocks();
vi.mocked(getSupportedMediaTypes).mockResolvedValue({
image: ['.jpg'],
sidecar: ['.xmp'],
video: ['.mp4'],
});
testFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'test-startWatch-'));
checkBulkUploadMocked = vi.mocked(checkBulkUpload);
checkBulkUploadMocked.mockResolvedValue({
results: [],
});
});
it('should start watching a directory and upload new files', async () => {
const testFilePath = path.join(testFolder, 'test.jpg');
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
await sleep(100); // to debounce the watcher from considering the test file as a existing file
await fs.promises.writeFile(testFilePath, 'testjpg');
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: [
expect.objectContaining({
id: testFilePath,
}),
],
},
});
});
it('should filter out unsupported files', async () => {
const testFilePath = path.join(testFolder, 'test.jpg');
const unsupportedFilePath = path.join(testFolder, 'test.txt');
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
await sleep(100); // to debounce the watcher from considering the test file as a existing file
await fs.promises.writeFile(testFilePath, 'testjpg');
await fs.promises.writeFile(unsupportedFilePath, 'testtxt');
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: testFilePath,
}),
]),
},
});
expect(checkBulkUpload).not.toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: unsupportedFilePath,
}),
]),
},
});
});
it('should filger out ignored patterns', async () => {
const testFilePath = path.join(testFolder, 'test.jpg');
const ignoredPattern = 'ignored';
const ignoredFolder = path.join(testFolder, ignoredPattern);
await fs.promises.mkdir(ignoredFolder, { recursive: true });
const ignoredFilePath = path.join(ignoredFolder, 'ignored.jpg');
await startWatch([testFolder], { concurrency: 1, ignore: ignoredPattern }, { batchSize: 1, debounceTimeMs: 10 });
await sleep(100); // to debounce the watcher from considering the test file as a existing file
await fs.promises.writeFile(testFilePath, 'testjpg');
await fs.promises.writeFile(ignoredFilePath, 'ignoredjpg');
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: testFilePath,
}),
]),
},
});
expect(checkBulkUpload).not.toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: ignoredFilePath,
}),
]),
},
});
});
afterEach(async () => {
await fs.promises.rm(testFolder, { recursive: true, force: true });
});
});

View File

@@ -1,6 +1,5 @@
import { import {
Action, Action,
AssetBulkUploadCheckItem,
AssetBulkUploadCheckResult, AssetBulkUploadCheckResult,
AssetMediaResponseDto, AssetMediaResponseDto,
AssetMediaStatus, AssetMediaStatus,
@@ -12,18 +11,13 @@ import {
getSupportedMediaTypes, getSupportedMediaTypes,
} from '@immich/sdk'; } from '@immich/sdk';
import byteSize from 'byte-size'; import byteSize from 'byte-size';
import { Matcher, watch as watchFs } from 'chokidar'; import { Presets, SingleBar } from 'cli-progress';
import { MultiBar, Presets, SingleBar } from 'cli-progress';
import { chunk } from 'lodash-es'; import { chunk } from 'lodash-es';
import micromatch from 'micromatch';
import { Stats, createReadStream } from 'node:fs'; import { Stats, createReadStream } from 'node:fs';
import { stat, unlink } from 'node:fs/promises'; import { stat, unlink } from 'node:fs/promises';
import path, { basename } from 'node:path'; import path, { basename } from 'node:path';
import { Queue } from 'src/queue'; import { Queue } from 'src/queue';
import { BaseOptions, Batcher, authenticate, crawl, sha1 } from 'src/utils'; import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
const UPLOAD_WATCH_BATCH_SIZE = 100;
const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000;
const s = (count: number) => (count === 1 ? '' : 's'); const s = (count: number) => (count === 1 ? '' : 's');
@@ -41,9 +35,6 @@ export interface UploadOptionsDto {
albumName?: string; albumName?: string;
includeHidden?: boolean; includeHidden?: boolean;
concurrency: number; concurrency: number;
progress?: boolean;
watch?: boolean;
jsonOutput?: boolean;
} }
class UploadFile extends File { class UploadFile extends File {
@@ -63,100 +54,19 @@ class UploadFile extends File {
} }
} }
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
const { newFiles, duplicates } = await checkForDuplicates(files, options);
const newAssets = await uploadFiles(newFiles, options);
if (options.jsonOutput) {
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
}
await updateAlbums([...newAssets, ...duplicates], options);
await deleteFiles(
newAssets.map(({ filepath }) => filepath),
options,
);
};
export const startWatch = async (
paths: string[],
options: UploadOptionsDto,
{
batchSize = UPLOAD_WATCH_BATCH_SIZE,
debounceTimeMs = UPLOAD_WATCH_DEBOUNCE_TIME_MS,
}: { batchSize?: number; debounceTimeMs?: number } = {},
) => {
const watcherIgnored: Matcher[] = [];
const { image, video } = await getSupportedMediaTypes();
const extensions = new Set([...image, ...video]);
if (options.ignore) {
watcherIgnored.push((path) => micromatch.contains(path, `**/${options.ignore}`));
}
const pathsBatcher = new Batcher<string>({
batchSize,
debounceTimeMs,
onBatch: async (paths: string[]) => {
const uniquePaths = [...new Set(paths)];
await uploadBatch(uniquePaths, options);
},
});
const onFile = async (path: string, stats?: Stats) => {
if (stats?.isDirectory()) {
return;
}
const ext = '.' + path.split('.').pop()?.toLowerCase();
if (!ext || !extensions.has(ext)) {
return;
}
if (!options.progress) {
// logging when progress is disabled as it can cause issues with the progress bar rendering
console.log(`Change detected: ${path}`);
}
pathsBatcher.add(path);
};
const fsWatcher = watchFs(paths, {
ignoreInitial: true,
ignored: watcherIgnored,
alwaysStat: true,
awaitWriteFinish: true,
depth: options.recursive ? undefined : 1,
persistent: true,
})
.on('add', onFile)
.on('change', onFile)
.on('error', (error) => console.error(`Watcher error: ${error}`));
process.on('SIGINT', async () => {
console.log('Exiting...');
await fsWatcher.close();
process.exit();
});
};
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
await authenticate(baseOptions); await authenticate(baseOptions);
const scanFiles = await scan(paths, options); const scanFiles = await scan(paths, options);
if (scanFiles.length === 0) { if (scanFiles.length === 0) {
if (options.watch) { console.log('No files found, exiting');
console.log('No files found initially.'); return;
} else {
console.log('No files found, exiting');
return;
}
} }
if (options.watch) { const { newFiles, duplicates } = await checkForDuplicates(scanFiles, options);
console.log('Watching for changes...'); const newAssets = await uploadFiles(newFiles, options);
await startWatch(paths, options); await updateAlbums([...newAssets, ...duplicates], options);
// watcher does not handle the initial scan await deleteFiles(newFiles, options);
// as the scan() is a more efficient quick start with batched results
}
await uploadBatch(scanFiles, options);
}; };
const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => { const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
@@ -174,35 +84,29 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
return files; return files;
}; };
export const checkForDuplicates = async (files: string[], { concurrency, skipHash, progress }: UploadOptionsDto) => { export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
if (skipHash) { if (skipHash) {
console.log('Skipping hash check, assuming all files are new'); console.log('Skipping hash check, assuming all files are new');
return { newFiles: files, duplicates: [] }; return { newFiles: files, duplicates: [] };
} }
let multiBar: MultiBar | undefined; const progressBar = new SingleBar(
{ format: 'Checking files | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
Presets.shades_classic,
);
if (progress) { progressBar.start(files.length, 0);
multiBar = new MultiBar(
{ format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
Presets.shades_classic,
);
} else {
console.log(`Received ${files.length} files, hashing...`);
}
const hashProgressBar = multiBar?.create(files.length, 0, { message: 'Hashing files ' });
const checkProgressBar = multiBar?.create(files.length, 0, { message: 'Checking for duplicates' });
const newFiles: string[] = []; const newFiles: string[] = [];
const duplicates: Asset[] = []; const duplicates: Asset[] = [];
const checkBulkUploadQueue = new Queue<AssetBulkUploadCheckItem[], void>( const queue = new Queue<string[], AssetBulkUploadCheckResults>(
async (assets: AssetBulkUploadCheckItem[]) => { async (filepaths: string[]) => {
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets } }); const dto = await Promise.all(
filepaths.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })),
);
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
const results = response.results as AssetBulkUploadCheckResults; const results = response.results as AssetBulkUploadCheckResults;
for (const { id: filepath, assetId, action } of results) { for (const { id: filepath, assetId, action } of results) {
if (action === Action.Accept) { if (action === Action.Accept) {
newFiles.push(filepath); newFiles.push(filepath);
@@ -211,46 +115,19 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
duplicates.push({ id: assetId as string, filepath }); duplicates.push({ id: assetId as string, filepath });
} }
} }
progressBar.increment(filepaths.length);
checkProgressBar?.increment(assets.length);
},
{ concurrency, retry: 3 },
);
const results: { id: string; checksum: string }[] = [];
let checkBulkUploadRequests: AssetBulkUploadCheckItem[] = [];
const queue = new Queue<string, AssetBulkUploadCheckItem[]>(
async (filepath: string): Promise<AssetBulkUploadCheckItem[]> => {
const dto = { id: filepath, checksum: await sha1(filepath) };
results.push(dto);
checkBulkUploadRequests.push(dto);
if (checkBulkUploadRequests.length === 5000) {
const batch = checkBulkUploadRequests;
checkBulkUploadRequests = [];
void checkBulkUploadQueue.push(batch);
}
hashProgressBar?.increment();
return results; return results;
}, },
{ concurrency, retry: 3 }, { concurrency, retry: 3 },
); );
for (const item of files) { for (const items of chunk(files, concurrency)) {
void queue.push(item); await queue.push(items);
} }
await queue.drained(); await queue.drained();
if (checkBulkUploadRequests.length > 0) { progressBar.stop();
void checkBulkUploadQueue.push(checkBulkUploadRequests);
}
await checkBulkUploadQueue.drained();
multiBar?.stop();
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`); console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
@@ -266,10 +143,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
return { newFiles, duplicates }; return { newFiles, duplicates };
}; };
export const uploadFiles = async ( export const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
files: string[],
{ dryRun, concurrency, progress }: UploadOptionsDto,
): Promise<Asset[]> => {
if (files.length === 0) { if (files.length === 0) {
console.log('All assets were already uploaded, nothing to do.'); console.log('All assets were already uploaded, nothing to do.');
return []; return [];
@@ -289,20 +163,12 @@ export const uploadFiles = async (
return files.map((filepath) => ({ id: '', filepath })); return files.map((filepath) => ({ id: '', filepath }));
} }
let uploadProgress: SingleBar | undefined; const uploadProgress = new SingleBar(
{ format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}' },
if (progress) { Presets.shades_classic,
uploadProgress = new SingleBar( );
{ uploadProgress.start(totalSize, 0);
format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}', uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
},
Presets.shades_classic,
);
} else {
console.log(`Uploading ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`);
}
uploadProgress?.start(totalSize, 0);
uploadProgress?.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
let duplicateCount = 0; let duplicateCount = 0;
let duplicateSize = 0; let duplicateSize = 0;
@@ -328,20 +194,20 @@ export const uploadFiles = async (
successSize += stats.size ?? 0; successSize += stats.size ?? 0;
} }
uploadProgress?.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) }); uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
return response; return response;
}, },
{ concurrency, retry: 3 }, { concurrency, retry: 3 },
); );
for (const item of files) { for (const filepath of files) {
void queue.push(item); await queue.push(filepath);
} }
await queue.drained(); await queue.drained();
uploadProgress?.stop(); uploadProgress.stop();
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`); console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
if (duplicateCount > 0) { if (duplicateCount > 0) {

View File

@@ -68,19 +68,7 @@ program
.env('IMMICH_UPLOAD_CONCURRENCY') .env('IMMICH_UPLOAD_CONCURRENCY')
.default(4), .default(4),
) )
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS')) .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
.addOption(
new Option('--watch', 'Watch for changes and upload automatically')
.env('IMMICH_WATCH_CHANGES')
.default(false)
.implies({ progress: false }),
)
.argument('[paths...]', 'One or more paths to assets to be uploaded') .argument('[paths...]', 'One or more paths to assets to be uploaded')
.action((paths, options) => upload(paths, program.opts(), options)); .action((paths, options) => upload(paths, program.opts(), options));

View File

@@ -72,8 +72,8 @@ export class Queue<T, R> {
* @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker. * @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
* This promise could be ignored as it will not lead to a `unhandledRejection`. * This promise could be ignored as it will not lead to a `unhandledRejection`.
*/ */
drained(): Promise<void> { async drained(): Promise<void> {
return this.queue.drained(); await this.queue.drain();
} }
/** /**

View File

@@ -1,13 +1,11 @@
import mockfs from 'mock-fs'; import mockfs from 'mock-fs';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { Batcher, CrawlOptions, crawl } from 'src/utils'; import { CrawlOptions, crawl } from 'src/utils';
import { Mock } from 'vitest';
interface Test { interface Test {
test: string; test: string;
options: Omit<CrawlOptions, 'extensions'>; options: Omit<CrawlOptions, 'extensions'>;
files: Record<string, boolean>; files: Record<string, boolean>;
skipOnWin32?: boolean;
} }
const cwd = process.cwd(); const cwd = process.cwd();
@@ -50,18 +48,6 @@ const tests: Test[] = [
'/photos/image.jpg': true, '/photos/image.jpg': true,
}, },
}, },
{
test: 'should crawl folders with quotes',
options: {
pathsToCrawl: ["/photo's/", '/photo"s/', '/photo`s/'],
},
files: {
"/photo's/image1.jpg": true,
'/photo"s/image2.jpg': true,
'/photo`s/image3.jpg': true,
},
skipOnWin32: true, // single quote interferes with mockfs root on Windows
},
{ {
test: 'should crawl a single file', test: 'should crawl a single file',
options: { options: {
@@ -129,7 +115,17 @@ const tests: Test[] = [
'/albums/image3.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', test: 'should crawl a single path without trailing slash',
options: { options: {
@@ -284,12 +280,8 @@ describe('crawl', () => {
}); });
describe('crawl', () => { describe('crawl', () => {
for (const { test: name, options, files, skipOnWin32 } of tests) { for (const { test, options, files } of tests) {
if (process.platform === 'win32' && skipOnWin32) { it(test, async () => {
test.skip(name);
continue;
}
it(name, async () => {
// The file contents is the same as the path. // The file contents is the same as the path.
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file]))); mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file])));
@@ -304,38 +296,3 @@ describe('crawl', () => {
} }
}); });
}); });
describe('Batcher', () => {
let batcher: Batcher;
let onBatch: Mock;
beforeEach(() => {
onBatch = vi.fn();
batcher = new Batcher({ batchSize: 2, onBatch });
});
it('should trigger onBatch() when a batch limit is reached', async () => {
batcher.add('a');
batcher.add('b');
batcher.add('c');
expect(onBatch).toHaveBeenCalledOnce();
expect(onBatch).toHaveBeenCalledWith(['a', 'b']);
});
it('should trigger onBatch() when flush() is called', async () => {
batcher.add('a');
batcher.flush();
expect(onBatch).toHaveBeenCalledOnce();
expect(onBatch).toHaveBeenCalledWith(['a']);
});
it('should trigger onBatch() when debounce time reached', async () => {
vi.useFakeTimers();
batcher = new Batcher({ batchSize: 2, debounceTimeMs: 100, onBatch });
batcher.add('a');
expect(onBatch).not.toHaveBeenCalled();
vi.advanceTimersByTime(200);
expect(onBatch).toHaveBeenCalledOnce();
expect(onBatch).toHaveBeenCalledWith(['a']);
vi.useRealTimers();
});
});

View File

@@ -141,21 +141,25 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
} }
} }
if (patterns.length === 0) { let searchPattern: string;
if (patterns.length === 1) {
searchPattern = patterns[0];
} else if (patterns.length === 0) {
return crawledFiles; return crawledFiles;
} else {
searchPattern = '{' + patterns.join(',') + '}';
} }
const searchPatterns = patterns.map((pattern) => { if (recursive) {
let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]'); searchPattern = searchPattern + '/**/';
if (recursive) { }
escapedPattern = escapedPattern + '/**';
}
return `${escapedPattern}/*.{${extensions.join(',')}}`;
});
const globbedFiles = await glob(searchPatterns, { searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`;
const globbedFiles = await glob(searchPattern, {
absolute: true, absolute: true,
caseSensitiveMatch: false, caseSensitiveMatch: false,
onlyFiles: true,
dot: includeHidden, dot: includeHidden,
ignore: [`**/${exclusionPattern}`], ignore: [`**/${exclusionPattern}`],
}); });
@@ -172,64 +176,3 @@ export const sha1 = (filepath: string) => {
rs.on('end', () => resolve(hash.digest('hex'))); rs.on('end', () => resolve(hash.digest('hex')));
}); });
}; };
/**
* Batches items and calls onBatch to process them
* when the batch size is reached or the debounce time has passed.
*/
export class Batcher<T = unknown> {
private items: T[] = [];
private readonly batchSize: number;
private readonly debounceTimeMs?: number;
private readonly onBatch: (items: T[]) => void;
private debounceTimer?: NodeJS.Timeout;
constructor({
batchSize,
debounceTimeMs,
onBatch,
}: {
batchSize: number;
debounceTimeMs?: number;
onBatch: (items: T[]) => Promise<void>;
}) {
this.batchSize = batchSize;
this.debounceTimeMs = debounceTimeMs;
this.onBatch = onBatch;
}
private setDebounceTimer() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
if (this.debounceTimeMs) {
this.debounceTimer = setTimeout(() => this.flush(), this.debounceTimeMs);
}
}
private clearDebounceTimer() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = undefined;
}
}
add(item: T) {
this.items.push(item);
this.setDebounceTimer();
if (this.items.length >= this.batchSize) {
this.flush();
}
}
flush() {
this.clearDebounceTimer();
if (this.items.length === 0) {
return;
}
this.onBatch(this.items);
this.items = [];
}
}

View File

@@ -1,4 +0,0 @@
export CLOUDFLARE_ACCOUNT_ID="op://tf/cloudflare/account_id"
export CLOUDFLARE_API_TOKEN="op://tf/cloudflare/api_token"
export TF_STATE_POSTGRES_CONN_STR="op://tf/tf_state/postgres_conn_str"
export TF_VAR_env=$ENVIRONMENT

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.5" version = "4.38.0"
constraints = "4.52.5" constraints = "4.38.0"
hashes = [ hashes = [
"h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=", "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=",
"h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=",
"h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=", "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=",
"h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=", "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=",
"h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=", "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=",
"h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=", "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=",
"h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=", "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=",
"h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=", "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=",
"h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=", "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=",
"h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=", "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=",
"h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=", "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=",
"h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=", "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=",
"h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=", "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=",
"h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=", "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=",
"zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071",
"zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979",
"zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567",
"zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965",
"zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a",
"zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072", "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607",
"zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661", "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df",
"zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805",
"zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988",
"zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba",
"zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d",
"zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02",
"zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf",
"zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722",
] ]
} }

View File

@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.52.5" version = "4.38.0"
} }
} }
} }

View File

@@ -1,14 +1,14 @@
resource "cloudflare_pages_domain" "immich_app_release_domain" { resource "cloudflare_pages_domain" "immich_app_release_domain" {
account_id = var.cloudflare_account_id account_id = var.cloudflare_account_id
project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name
domain = "docs.immich.app" domain = "immich.app"
} }
resource "cloudflare_record" "immich_app_release_domain" { resource "cloudflare_record" "immich_app_release_domain" {
name = "docs.immich.app" name = "immich.app"
proxied = true proxied = true
ttl = 1 ttl = 1
type = "CNAME" type = "CNAME"
content = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
} }

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.5" version = "4.38.0"
constraints = "4.52.5" constraints = "4.38.0"
hashes = [ hashes = [
"h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=", "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=",
"h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=", "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=",
"h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=", "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=",
"h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=", "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=",
"h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=", "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=",
"h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=", "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=",
"h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=", "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=",
"h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=", "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=",
"h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=", "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=",
"h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=", "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=",
"h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=", "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=",
"h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=", "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=",
"h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=", "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=",
"h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=", "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=",
"zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b", "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071",
"zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a", "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979",
"zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7", "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567",
"zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238", "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965",
"zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b", "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a",
"zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072", "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607",
"zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661", "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df",
"zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54", "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805",
"zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852", "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988",
"zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0", "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba",
"zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5", "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d",
"zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036", "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02",
"zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803", "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf",
"zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722",
] ]
} }

View File

@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.52.5" version = "4.38.0"
} }
} }
} }

View File

@@ -1,15 +1,15 @@
resource "cloudflare_pages_domain" "immich_app_branch_domain" { resource "cloudflare_pages_domain" "immich_app_branch_domain" {
account_id = var.cloudflare_account_id account_id = var.cloudflare_account_id
project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name
domain = "docs.${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
} }
resource "cloudflare_record" "immich_app_branch_subdomain" { resource "cloudflare_record" "immich_app_branch_subdomain" {
name = "docs.${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
proxied = true proxied = true
ttl = 1 ttl = 1
type = "CNAME" type = "CNAME"
content = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}" value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
} }
@@ -18,7 +18,7 @@ output "immich_app_branch_subdomain" {
} }
output "immich_app_branch_pages_hostname" { output "immich_app_branch_pages_hostname" {
value = cloudflare_record.immich_app_branch_subdomain.content value = cloudflare_record.immich_app_branch_subdomain.value
} }
output "pages_project_name" { output "pages_project_name" {

View File

@@ -1,13 +1,4 @@
# # See:
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# 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.
# For development see:
# - https://immich.app/docs/developer/setup # - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting # - https://immich.app/docs/developer/troubleshooting
@@ -16,31 +7,23 @@ name: immich-dev
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
command: ['immich-dev'] command: ['/usr/src/app/bin/immich-dev']
image: immich-server-dev:latest image: immich-server-dev:latest
# extends: # extends:
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile.dev dockerfile: server/Dockerfile
target: dev target: dev
restart: unless-stopped restart: always
volumes: volumes:
- ..:/usr/src/app - ../server:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/data - ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
env_file: env_file:
- .env - .env
environment: environment:
@@ -53,72 +36,51 @@ services:
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849 IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
IMMICH_BUILD_IMAGE: development IMMICH_BUILD_IMAGE: development
IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server
IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/community-guides
ulimits: ulimits:
nofile: nofile:
soft: 1048576 soft: 1048576
hard: 1048576 hard: 1048576
ports: ports:
- 3001:3001
- 9230:9230 - 9230:9230
- 9231:9231
- 2283:2283
depends_on: depends_on:
redis: - redis
condition: service_started - database
database:
condition: service_started
healthcheck:
disable: false
immich-web: immich-web:
container_name: immich_web container_name: immich_web
image: immich-web-dev:latest image: immich-web-dev:latest
build: build:
context: ../ context: ../web
dockerfile: server/Dockerfile.dev command: ['/usr/src/app/bin/immich-web']
target: dev
command: ['immich-web']
env_file: env_file:
- .env - .env
ports: ports:
- 3000:3000 - 2283:3000
- 24678:24678 - 24678:24678
volumes: volumes:
- ..:/usr/src/app - ../web:/usr/src/app
- pnpm-store:/usr/src/app/.pnpm-store - ../open-api/:/usr/src/open-api/
- server-node_modules:/usr/src/app/server/node_modules - /usr/src/app/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
ulimits: ulimits:
nofile: nofile:
soft: 1048576 soft: 1048576
hard: 1048576 hard: 1048576
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
immich-server: - immich-server
condition: service_started
immich-machine-learning: immich-machine-learning:
container_name: immich_machine_learning container_name: immich_machine_learning
image: immich-machine-learning-dev:latest image: immich-machine-learning-dev:latest
# extends: # extends:
# file: hwaccel.ml.yml # file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference # service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
build: build:
context: ../machine-learning context: ../machine-learning
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
- DEVICE=cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
ports: ports:
- 3003:3003 - 3003:3003
volumes: volumes:
@@ -129,18 +91,16 @@ services:
depends_on: depends_on:
- database - database
restart: unless-stopped restart: unless-stopped
healthcheck:
disable: false
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
env_file: env_file:
- .env - .env
environment: environment:
@@ -152,8 +112,29 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
shm_size: 128mb healthcheck:
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command:
[
'postgres',
'-c',
'shared_preload_libraries=vectors.so',
'-c',
'search_path="$$user", public, vectors',
'-c',
'logging_collector=on',
'-c',
'max_wal_size=2GB',
'-c',
'shared_buffers=512MB',
'-c',
'wal_compression=on',
]
# set IMMICH_METRICS=true in .env to enable metrics
# immich-prometheus: # immich-prometheus:
# container_name: immich_prometheus # container_name: immich_prometheus
# ports: # ports:
@@ -178,14 +159,3 @@ volumes:
model-cache: model-cache:
prometheus-data: prometheus-data:
grafana-data: grafana-data:
pnpm-store:
server-node_modules:
web-node_modules:
github-node_modules:
cli-node_modules:
docs-node_modules:
e2e-node_modules:
sdk-node_modules:
app-node_modules:
sveltekit:
coverage:

View File

@@ -1,12 +1,3 @@
#
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# 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-prod name: immich-prod
services: services:
@@ -20,30 +11,28 @@ services:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
volumes: volumes:
- ${UPLOAD_LOCATION}/photos:/data - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
ports: ports:
- 2283:2283 - 2283:3001
depends_on: depends_on:
- redis - redis
- database - database
restart: always restart: always
healthcheck:
disable: false
immich-machine-learning: immich-machine-learning:
container_name: immich_machine_learning container_name: immich_machine_learning
image: immich-machine-learning:latest image: immich-machine-learning:latest
# extends: # extends:
# file: hwaccel.ml.yml # file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference # service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
build: build:
context: ../machine-learning context: ../machine-learning
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
- DEVICE=cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
ports: ports:
- 3003:3003 - 3003:3003
volumes: volumes:
@@ -51,19 +40,17 @@ services:
env_file: env_file:
- .env - .env
restart: always restart: always
healthcheck:
disable: false
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
env_file: env_file:
- .env - .env
environment: environment:
@@ -75,15 +62,20 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
shm_size: 128mb healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always restart: always
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # set IMMICH_METRICS=true in .env to enable metrics
immich-prometheus: immich-prometheus:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996 image: prom/prometheus@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
@@ -95,7 +87,7 @@ services:
command: ['./run.sh', '-disable-reporting'] command: ['./run.sh', '-disable-reporting']
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:12.1.1-ubuntu@sha256:d1da838234ff2de93e0065ee1bf0e66d38f948dcc5d718c25fa6237e14b4424a image: grafana/grafana:11.1.0-ubuntu@sha256:c7fc29ec783d5e7fc1bdfaad6f92345a345cffbc5d21c388ca228175006fc107
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana

View File

@@ -1,11 +1,10 @@
# #
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose # WARNING: Make sure to use the docker-compose.yml of the current release:
#
# Make sure to use the docker-compose.yml of the current release:
# #
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml # 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. # The compose file on main may not be compatible with the latest release.
#
name: immich name: immich
@@ -17,57 +16,54 @@ services:
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes: volumes:
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file - ${UPLOAD_LOCATION}:/usr/src/app/upload
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
ports: ports:
- '2283:2283' - 2283:3001
depends_on: depends_on:
- redis - redis
- database - database
restart: always restart: always
healthcheck:
disable: false
immich-machine-learning: immich-machine-learning:
container_name: immich_machine_learning container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag. # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
# Example tag: ${IMMICH_VERSION:-release}-cuda # Example tag: ${IMMICH_VERSION:-release}-cuda
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
# file: hwaccel.ml.yml # file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable # service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
volumes: volumes:
- model-cache:/cache - model-cache:/cache
env_file: env_file:
- .env - .env
restart: always restart: always
healthcheck:
disable: false
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84 image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums' POSTGRES_INITDB_ARGS: '--data-checksums'
# Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
# DB_STORAGE_TYPE: 'HDD'
volumes: volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always restart: always
volumes: volumes:

View File

@@ -2,8 +2,7 @@
# The location where your uploaded files are stored # The location where your uploaded files are stored
UPLOAD_LOCATION=./library UPLOAD_LOCATION=./library
# The location where your database files are stored
# The location where your database files are stored. Network shares are not supported for the database
DB_DATA_LOCATION=./postgres DB_DATA_LOCATION=./postgres
# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List # To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
@@ -13,7 +12,6 @@ DB_DATA_LOCATION=./postgres
IMMICH_VERSION=release IMMICH_VERSION=release
# Connection secret for postgres. You should change it to a random password # Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
DB_PASSWORD=postgres DB_PASSWORD=postgres
# The values below this line do not need to be changed # The values below this line do not need to be changed

View File

@@ -13,13 +13,6 @@ services:
volumes: volumes:
- /lib/firmware/mali_csffw.bin:/lib/firmware/mali_csffw.bin:ro # Mali firmware for your chipset (not always required depending on the driver) - /lib/firmware/mali_csffw.bin:/lib/firmware/mali_csffw.bin:ro # Mali firmware for your chipset (not always required depending on the driver)
- /usr/lib/libmali.so:/usr/lib/libmali.so:ro # Mali driver for your chipset (always required) - /usr/lib/libmali.so:/usr/lib/libmali.so:ro # Mali driver for your chipset (always required)
rknn:
security_opt:
- systempaths=unconfined
- apparmor=unconfined
devices:
- /dev/dri:/dev/dri
cpu: {} cpu: {}
@@ -33,13 +26,6 @@ services:
capabilities: capabilities:
- gpu - gpu
rocm:
group_add:
- video
devices:
- /dev/dri:/dev/dri
- /dev/kfd:/dev/kfd
openvino: openvino:
device_cgroup_rules: device_cgroup_rules:
- 'c 189:* rmw' - 'c 189:* rmw'

View File

@@ -48,8 +48,8 @@ services:
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2 vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
devices: devices:
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
- /dev/dxg:/dev/dxg
volumes: volumes:
- /usr/lib/wsl:/usr/lib/wsl - /usr/lib/wsl:/usr/lib/wsl
environment: environment:
- LD_LIBRARY_PATH=/usr/lib/wsl/lib
- LIBVA_DRIVER_NAME=d3d12 - LIBVA_DRIVER_NAME=d3d12

4
docs/.gitignore vendored
View File

@@ -18,6 +18,4 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
yarn.lock yarn.lock
/static/openapi.json

View File

@@ -1 +1 @@
22.19.0 20.16.0

View File

@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
### Installation ### Installation
``` ```
$ pnpm install $ npm install
``` ```
### Local Development ### Local Development
``` ```
$ pnpm run start $ npm run start
``` ```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
### Build ### Build
``` ```
$ pnpm run build $ npm run build
``` ```
This command generates static content into the `build` directory and can be served using any static contents hosting service. This command generates static content into the `build` directory and can be served using any static contents hosting service.
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
Using SSH: Using SSH:
``` ```
$ USE_SSH=true pnpm run deploy $ USE_SSH=true npm run deploy
``` ```
Not using SSH: Not using SSH:
``` ```
$ GIT_USER=<Your GitHub username> pnpm run deploy $ GIT_USER=<Your GitHub username> npm run deploy
``` ```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@@ -0,0 +1,110 @@
---
slug: release-1-36
title: Release v1.36.0
authors: [alextran]
tags: [release]
date: 2022-11-10
---
Hello everyone, it is my pleasure to deliver the new release of Immich to you. The team has been working hard to bring you the new features and improvements. This release includes some big features that the community has been asking since the beginning of Immich. We hope you will enjoy it.
Some notable features are:
- OAuth integration
- LivePhoto support on iOS
- User config system
<!--truncate-->
## LivePhoto iOS Support 🎉
LivePhoto on iOS is now supported in Immich.
The motion part will now be uploaded and can be played on the mobile app and the web.
:::caution
- The server and the app has to be on version **1.36.x** for the application to work correctly.
- Previous uploaded photos will not be updated automatically, you will have to remove and reupload them if you want to keep the LivePhoto functionality.
:::
<img
src="https://media.giphy.com/media/fTrGceZd7t1ewi8ESc/giphy.gif"
width="100%"
style={{
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
title="LivePhoto playback on the web"
/>
## OAuth Integration 🎉
I want to borrow this chance to express my gratitude to [@EnricoBilla](https://github.com/EnricoBilla), who has been the trailblazer for this feature since the beginning days of Immich. His PR has sparked ideas, suggestions, and discussion among the team member on how to integrate this feature successfully into the app. Thank you so much for your work and your time.
OAuth is now integrated into the system. Please follow the guide [here](https://immich.app/docs/usage/oauth) to set up your OAuth integration
After setting up the correct environment variables in the `.env` file, as shown below
| Key | Type | Default | Description |
| ------------------- | ------- | -------------------- | ------------------------------------------------------------------------- |
| OAUTH_ENABLED | boolean | false | Enable/disable OAuth2 |
| OAUTH_ISSUER_URL | URL | (required) | Required. Self-discovery URL for client |
| OAUTH_CLIENT_ID | string | (required) | Required. Client ID |
| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret |
| OAUTH_SCOPE | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| OAUTH_AUTO_REGISTER | boolean | true | When true, will automatically register a user the first time they sign in |
| OAUTH_BUTTON_TEXT | string | Login with OAuth | Text for the OAuth button on the web |
```bash title="Authentik Example"
OAUTH_ENABLED=true
OAUTH_ISSUER_URL=http://10.1.15.216:9000/application/o/immich-test/
OAUTH_CLIENT_ID=30596v8f78a4b6a97d5985c3076b6b4c4d12ddc33
OAUTH_CLIENT_SECRET=50f1eafdec353b95b1c638db390db4ab67ef035a51212dbec2f56175e2eb272b5d572c099176e6fe116ecf47ffdd544bgdb9e2edc588307ee0339d25eeccd88
OAUTH_BUTTON_TEXT=Login with Authentik
```
The web will have the option to sign in with OAuth.
<img
src="https://user-images.githubusercontent.com/27055614/202923726-f43fa148-47f5-4182-8f29-b0b87e4586fa.png"
width="50%"
title="Web Sign in with OAuth"
style={{
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
/>
The mobile app will check if the server has OAuth enabled before displaying the OAuth
sign-in button.
<img
src="https://media.giphy.com/media/3iy3SaNkVYtlkEiw06/giphy.gif"
title="Mobile sign in with OAuth"
style={{
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
/>
## Support
<img
src="https://media.giphy.com/media/LStqgGESXW8XnuCv5y/giphy.gif"
width="300"
style={{
borderRadius: '10px',
boxShadow: 'rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px',
}}
title="Support the project"
/>
If you find the project helpful and it helps you in some ways, you can support the project [one time](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or [monthly](https://github.com/sponsors/alextran1502) from GitHub Sponsor
It is a great way to let me know that you want me to continue developing and working on this project for years to come.
## Details
For more details, please check out the [release note](https://github.com/immich-app/immich/releases/tag/v1.36.0_55-dev)

View File

@@ -0,0 +1,103 @@
---
title: Immich Update - June 2023
authors: [alextran]
tags: [update]
---
Hello everybody, Alex here!
I am back with another update on Immich. It has been only a month since my last update (May 18th, 2023), but it seems forever. I think the rapid releases of Immich and the amount of work make the perspective of time change in Immichs world. We have some exciting updates that I think you will like.
Before going into detail, on behalf of the core team, I would like to thank all of you for loving Immich and contributing to the project. Thank you for helping me make Immich an enjoyable alternative solution to Google Photos so that you have complete control of your data and privacy. I know we are still young and have a lot of work to do, but I am confident we will get there with help from the community. I appreciate all of you from the bottom of my heart!
<!--truncate-->
And now, to the exciting part, what is new in Immichs world?
- Initial support for existing gallery.
- Memory feature.
- Support XMP sidecar.
- Support more raw formats.
- Justified layout for web timeline and blurred thumbnail hash.
- Mechanism to host machine learning on a completely different machine.
## Support for existing gallery
I know this is the most controversial feature when it comes to Immichs way of ingesting photos and videos. For many users, having to upload photos and videos to Immich is simply not working. We listen, discuss, and digest this feature internally more than you imagine because it is not a simple feature to tackle while keeping the performance and the user experience at the top level, which is Immichs primary goal.
Thankfully, we have many great contributors and developers that want to make this come true. So we came up with an initial implementation of this feature in the form of a supporting read-only gallery.
To be concise, Immich can now read in the gallery files, register the path into the database, and then generate necessary files and put them through Immichs machine learning pipeline so you can use all the goodness of Immich without the need to upload them. Since this is the initial implementation, some actions/behavior are not yet supported, and we aim to build toward them in future releases, namely:
- Assets are not automatically synced and must instead be manually synced with the CLI tool.
- Only new files that are added to the gallery will be detected.
- Deleted and moved files will not be detected.
## 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.
This memory feature is very much similar to GPhotos' implementation of “x years since…”. We are aiming to add more categories of memories in the future, such as “Spotlight of the day” or “Day of the Week highlights”
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/j5XZKvViPew"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
This feature is now available on the web and will be ported to the mobile app in the near future.
## Support XMP Sidecar
Immich can now import/upload XMP sidecars from the CLI and use the information as the metadata of assets.
## Support more raw formats.
With the recent updates on the dependencies of Immich, we are now extending and hardening support for multiple raw formats. So users with DSLR or mirrorless cameras can now upload their original files to Immich and have them displayed in high-quality thumbnails on the web and mobile view.
## Justified layout for web timeline and blurred thumbnail hash
This is an aesthetic improvement in user experience when browsing the timeline. Photos and videos are now displayed correctly with perspective orientation, making the browsing experience more pleasurable.
To further improve the browsing experience, we now added a blur hash to the thumbnail, so the transition is more natural with a dreamy fade in effect, similar to how our brain goes from faded to vivid memory
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/b95FLmGHRFc"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
## Hosting machine learning container on a different machine
With more capabilities Immich is building toward, machine learning will get more powerful and therefore require more resources to run effectively. However, we understand that users might not have the best server resources where they host the Immich instance. Therefore, we changed how machine learning interacts and receives the photos and videos to run through its inference pipeline.
The machine learning container is now a headless system that can run on any machine. As long as your Immich instance can communicate with the system running the machine learning container, it can send the files and receive the required information to make Immich powerful in terms of searching and intelligence. This helps you to utilize a more powerful machine in your home/infrastructure to perform the CPU-intensive tasks while letting Immich only handle the I/O operations for a pleasant and smooth experience.
---
So, those are the highlights for the team and the community after a busy month. There are a lot more changes and improvements. I encourage you to read some release notes, starting from version [v1.57.0](https://github.com/immich-app/immich/releases/tag/v1.57.0) to now.
Thank you, and I am asking for your support for the project. I hope to be a full-time maintainer of Immich one day to dedicate myself to the project as my life works for the community and my family. You can find the support channels below:
- 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)
- [Liberapay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything
Cheer!
Until next time!
Alex

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -0,0 +1,151 @@
---
title: Immich Update - July 2023
authors: [alextran]
tags: [update, v1.64.0-v1.71.0]
---
Hello, Immich fans, another month, another milestone. We hope you are staying cool and safe in this scorching hot summer across the globe.
Immich recently got some good recognition when getting to the front page of HackerNews, which helped to let more people know about the project's existence. The project will help more and more people find a solution to control the privacy of their most precious moments. And with the gain in popularity and recognition, we have gotten new users and more questions from the community than ever.
I want to express my gratitude to all the contributors and the community who have been tremendously helpful to new users' questions and provided technical support.
Below are the highlights of new features we added to the application over the past month, along with countless bug fixes and improvements across the board, from developer experience to resource optimization and UI/UX improvement. I hope you find these topics as exciting as I am.
## Highlights
- Memories feature.
- Facial recognition improvements.
- Improvements on multi selection behavior on the web.
- Shortcuts for common actions on the web.
- Support viewer for 360-panorama photos.
<!--truncate-->
---
### Memories feature
We've added the memory feature on the mobile app, so you can reminisce about your past memories.
<iframe
width="560"
height="315"
src="https://youtube.com/embed/c7OTl-RqNRE"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
### Facial recognition improvements
Over the past few releases, we have added many UI improvements to the facial recognition feature to help you manage the recognized people better. Some of the highlights:
#### Choose a new feature photo for a person.
<iframe
width="560"
height="315"
src="https://youtube.com/embed/PmJp8DmSh1U"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
#### Hide and show faces.
You can now select irrelevant faces to hide them. The hidden faces wont be displayed in search results and the people section in the info panel.
#### Merge faces.
This is useful when you have multiple faces of the same person in your photos, and you want to merge them into one.
<iframe
width="560"
height="315"
src="https://youtube.com/embed/-Xskhw-vpc4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
We also added a nifty mechanism that when naming a face, similar names will prompt you a merge face option for the convenience.
<iframe
width="560"
height="315"
src="https://youtube.com/embed/XzE6wficbl4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
### Improvements on multi selection behavior on the web
We have added a new multi selection behavior on the web to help you select multiple items easier. You can now select a range of photos and videos by holding the `Shift` key.
<iframe
width="560"
height="315"
src="https://youtube.com/embed/e_SiuHpVnmM"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
### Shortcuts for common actions on the web.
Some of us only navigate the world and the web with a keyboard (looking at you, Vim and Emacs users). So it would take away the sacred weapon of choice to require many clicks to perform repetitive actions. So we added quick shortcuts for the following action on the web.
<img
src={require('./images/web-shortcuts-panel.png').default}
width="100%"
style={{ borderRadius: '25px' }}
alt="Dot Env Example"
/>
### Support viewer for 360-panorama photos.
Photos with the EXIF property of `ProjectionType` will now have a special viewer on the web to view all the angles of the panorama.
The thumbnail of the 360 degrees panoramas will have a special icon on the top right of the thumbnail
<img
src="https://github.com/immich-app/immich/assets/61410067/728ca1b0-375c-4631-8081-a609843e702f"
width="50%"
style={{ borderRadius: '25px' }}
alt="Dot Env Example"
/>
Panorama in the detail view
<img
src="https://github.com/immich-app/immich/assets/61410067/3c89dac4-395d-45fa-9bc5-98a6248fd476"
width="50%"
style={{ borderRadius: '25px' }}
alt="Dot Env Example"
/>
---
Thank you, and I am asking for your support for the project. I hope to be a full-time maintainer of Immich one day to dedicate myself to the project as my life's work for the community and my family. You can find the support channels below:
- 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)
- [Liberapay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything
Cheer!
Until next time!
Alex

View File

@@ -0,0 +1,71 @@
---
title: Immich Recap 2023
authors: [alextran]
tags: [update, recap-2023]
date: 2023-12-30T00:00
---
Hi everyone,
Alex from Immich here.
We are entering the last few weeks of 2023, and it has been quite a year for Immich. The project has grown so much in terms of users, developers, features, maturity, and the community around it. When I started working on Immich, it was simply a challenge for myself and an opportunity to learn new technologies, crafting something fun and useful for my wife during my free time to satisfy my urge to build and create things. I never thought it would become so popular and help so many people. At the end of the day, all we have is memory. I am proud that the team and I have created something to make storing and viewing those precious memories easier without restrictions and without sacrificing our privacy. As the year closes, heres a recap of everything the project accomplished in 2023.
# Milestones
- Public shared links
- Favorites page
- Immich turned 1
- Material Design 3 on the mobile app
- Auto-link LivePhotos server-side
- iOS background backup
- Explore page
- CLIP search
- Search by metadata
- Responsive web app
- Archive page
- Asset descriptions
- 10,000 stars on GitHub
- Manage auth devices
- Map view
- Facial recognition, clustering, searching, renaming, and person management
- Partner sharing and unifying timeline between partners' users
- Custom storage label
- XMP sidecar reading
- RAW file formats
- Justified layout on the web
- Memories
- Multi-select via SHIFT
- Android Motion Photos
- 360° Photos
- Album description
- Album performance improvements (time buckets)
- Video hardware transcoding
- Slideshow mode on the web
- Configuration file
- External libraries
- Trash page
- Custom theme
- Asset Stacking
- 20,000 stars on GitHub
- Shared album activity and comments
- CLI v2
- Down to 5 containers (from 8)
# Fun Statistics
- We have gone from the release version `1.41.0` to `1.90.0` at the time of writing. On average, we see a release every 7 days.
- According to GitHub's metrics, the `immich-server` container image has been pulled almost _4 million_ times.
- According to mobile app store metrics, we have 22,000 installations on Android and 6700 installation units on iOS (opt-in only).
- Immich is making around $1200/month on average from donations. (Thank you all so much!)
- We were guests on two podcasts:
- [Self-hosted](https://selfhosted.show/110)
- [The Vergecast](https://www.theverge.com/23938533/self-hosting-local-first-software-vergecast)
- There are over 4,500 members on the Discord server.
- We have over 22,000 stars on the main GitHub repository, gaining 15,000 stars since January 2023.
Diving into the next year, the team will continue to build on the foundation we have laid out over the past year, implementing more advanced features for searching, organizing, and sharing between users. Bugs will continue to be squashed and conquered. “Shit Alex wrote'' code will continue to be replaced by beautiful, clean code from Jason, Zack, Boet, Daniel, Osorin, Mert, Fynn, Marty, Martin, and Jonathan. The team has my eternal gratitude for creating a welcoming environment for new contributors, helping, teaching, and learning from each other. Ive realized that hardly a day has gone by where the team hasnt been in communication about Immich related topics over the past year.
My long-term goal is to help hone Immich into a diamond in the FOSS space, where the UI, UX, development experiences, documentation, and quality are at a high standard while remaining free for everybody to use.
I hope you enjoy Immich and have a happy and peaceful holiday.

View File

@@ -0,0 +1,75 @@
---
title: The Immich core team goes full-time
authors: [alextran]
tags: [update, announcement, FUTO]
date: 2024-05-01T00:00
---
**Immich is joining [FUTO](https://futo.org/)!**
Since the beginning of this adventure, my goal has always been to create a better world for my children. Memories are priceless, and privacy should not be a luxury. However, building quality open source has its challenges. Over the past two years, it has taken significant dedication, time, and effort.
Recently, a company in Austin, Texas, called FUTO contacted the team. FUTO strives to develop quality and sustainable open software. They build software alternatives that focus on giving control to users. From their mission statement:
“Computers should belong to you, the people. We develop and fund technology to give them back.”
FUTO loved Immich and wanted to see if wed consider working with them to take the project to the next level. In short, FUTO offered to:
- Pay the core team to work on Immich full-time
- Let us keep full autonomy about the projects direction and leadership
- Continue to license Immich under AGPL
- Keep Immichs development direction with no paywalled features
- Keep Immich “built for the people” (no ads, data mining/selling, or alternative motives)
- Provide us with financial, technical, legal, and administrative support
After careful deliberation, the team decided that FUTOs vision closely aligns with our own: to build a better future by providing a polished, performant, and privacy-preserving open-source software solution for photo and video management delivered in a sustainable way.
Immichs future has never looked brighter, and we look forward to realizing our vision for Immich as part of FUTO.
If you have more questions, well host a Q&A live stream on May 9th at 3PM UTC (10AM CST). [You can ask questions here](https://www.live-ask.com/event/01HWP2SB99A1K8EXFBDKZ5Z9CF), and the stream will be live [here on our YouTube channel](https://youtube.com/live/cwz2iZwYpgg).
Cheers,
The Immich Team
---
## FAQs
### What is FUTO?
[https://futo.org/what-is-futo/](https://futo.org/what-is-futo/)
### Will the license change?
No. Immich will continue to be licensed under AGPL without a CLA.
### Will Immich continue to be free?
Yes. The Immich source code will remain freely available under the AGPL license.
### Is Immich getting VC funding?
No. Venture capital implies investment in a business, often with the expectation of a future payout (exit plan). Immich is neither a business that can be acquired nor comes with a money-making exit plan.
### I am currently supporting Immich through GitHub sponsors. What will happen to my donation?
Effective immediately, all donations to the Immich organization will be canceled. In the future, we will offer an optional, modest payment option instead. Thank you to everyone who donated to help us get this far!
### How is funding sustainable?
Immich and FUTO believe a sustainable future requires a model that does not rely on users-as-a-product. To this end, FUTO advocates that users pay for good, open software. In keeping with this model, we will adopt a purchase price. This means we no longer accept donations, but — _without limiting features for those who do not pay_ — we will soon allow you to purchase Immich through a modest payment. We encourage you to pay for the high-quality software you use to foster a healthy software culture where developers build great applications without hidden motives for their users.
### When does this change take effect?
This change takes effect immediately.
### What will change?
The following things will change as Immich joins FUTO:
- The brand, logo, and other Immich trademarks will be transferred to FUTO.
- We will stop all donations to the project.
- The core team can now dedicate our full attention to Immich
- Before the end of the year, we plan to have a roadmap for what it will take to get Immich to a stable release.
- Bugs will be squashed, and features will be delivered faster.

View File

@@ -0,0 +1,91 @@
---
title: Licensing announcement - Purchase a license to support Immich
authors: [alextran]
tags: [update, announcement, FUTO]
date: 2024-07-18T00:00
---
Hello everybody,
Firstly, on behalf of the Immich team, I'd like to thank everybody for your continuous support of Immich since the very first day! Your contributions, encouragement, and community engagement have helped bring Immich to its current state. The team and I are forever grateful for that.
Since our [last announcement of the core team joining FUTO to work on Immich full-time](https://immich.app/blog/2024/immich-core-team-goes-fulltime), one of the goals of our new position is to foster a healthy relationship between the developers and the users. We believe that this enables us to create great software, establish transparent policies and build trust.
We want to build a great software application that brings value to you and your loved ones' lives. We are not using you as a product, i.e., selling or tracking your data. We are not putting annoying ads into our software. We respect your privacy. We want to be compensated for the hard work we put in to build Immich for you.
With those notes, we have enabled a way for you to financially support the continued development of Immich, ensuring the software can move forward and will be maintained, by offering a lifetime license of the software. We think if you like and use software, you should pay for it, but _we're never going to force anyone to pay or try to limit Immich for those who don't._
There are two types of license that you can choose to purchase: **Server License** and **Individual License**.
### Server License
This is a lifetime license costing **$99.99**. The license is applied to the whole server. You and all users that use your server are licensed.
### Individual License
This is a lifetime license costing **$24.99**. The license is applied to a single user, and can be used on any server they choose to connect to.
<img
width="837"
alt="license-social-gh"
src="https://github.com/user-attachments/assets/241932ed-ef3b-44ec-a9e2-ee80754e0cca"
/>
You can purchase the license on [our page - https://buy.immich.app](https://buy.immich.app).
Starting with release `v1.109.0` you can purchase and enter your purchased license key directly in the app.
<img
width="1414"
alt="license-page-gh"
src="https://github.com/user-attachments/assets/364fc32a-f6ef-4594-9fea-28d5a26ad77c"
/>
## Thank you
Thank you again for your support, this will help create a strong foundation and stability for the Immich team to continue developing and maintaining the project that you love to use.
<p align="center">
<img
src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbjY2eWc5Y2F0ZW56MmR4aWE0dDhzZXlidXRmYWZyajl1bWZidXZpcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/87CKDqErVfMqY/giphy.gif"
width="550"
title="SUPPORT THE PROJECT!"
/>
</p>
<br />
<br />
Cheers! 🎉
Immich team
# FAQ
### 1. Where can I purchase a license?
There are several places where you can purchase the license from
- [https://buy.immich.app](https://buy.immich.app)
- [https://pay.futo.org](https://pay.futo.org/)
- or directly from the app.
### 2. Do I need both _Individual License_ and _Server License_?
No,
If you are the admin and the sole user, or your instance has less than a total of 4 users, you can buy the **Individual License** for each user.
If your instance has more than 4 users, it is more cost-effective to buy the **Server License**, which will license all the users on your instance.
### 3. What do I do if I don't pay?
You can continue using Immich without any restriction.
### 4. Will there be any paywalled features?
No, there will never be any paywalled features.
### 5. Where can I get support regarding payment issues?
You can email us with your `orderId` and your email address `billing@futo.org` or on our Discord server.

View File

@@ -0,0 +1,78 @@
---
title: Immich Update - July 2024
authors: [alextran]
date: 2024-07-01T00:00
tags: [update, v1.106.0]
---
Hello everybody! Alex from Immich here and I am back with another development progress update for the project.
Summer has returned once again, and the night sky is filled with stars, thank you for **38_000 shining stars** you have sent to our [GitHub repo](https://github.com/immich-app/immich)! Since the last announcement several core contributors have started full time. Everything is going great with development, PRs get merged with _brrrrrrr_ rate, conversation exchange between team members is on a new high, we met and are working with the great engineers at FUTO. The spirit is high and we have a lot of things brewing that we think you will like.
Let's go over some of the updates we had since the last post.
### Container consolidation
Reduced the number of total containers from 5 to 4 by making the microservices thread get spawned directly in the server container. Woohoo, remember when Immich had 7 containers?
### Email notifications
![smtp](https://github.com/immich-app/immich/assets/27055614/949cba85-d3f1-4cd3-b246-a6f5fb5d3ae8)
We added email notifications to the app with SMTP settings that you can configure for the following events
- A new account is created for you.
- You are added to a shared album.
- New media is added to an album.
### Versioned docs
You can now jump back into the past or take a peek at the unreleased version of the documentation by selecting the version on the website.
![version-doc](https://github.com/immich-app/immich/assets/27055614/6d22898a-5093-41ad-b416-4573d7ce6e03)
### Similarity deduplication
With more machine learning and CLIP magic, we now have similarity deduplication built into the application where it will search for closely similar images and let you decide what to do with them; i.e keep or trash.
![similarity-deduplication](https://github.com/immich-app/immich/assets/27055614/3cac8478-fbf7-47ea-acb6-0146901dc67e)
### Permanent URL for asset on the web
The detail view for an asset now has a permanent URL so you can easily share them with your loved ones.
### Web app translations
We now have a public Weblate project which the community can use to translate the webapp to their native languages. We are planning to port the mobile app translation to this platform as well. If you would like to contribute, you can take a look [here](https://hosted.weblate.org/projects/immich/immich/). We're already close to 50% translations -- we really appreciate everyone contributing to that!
![web-translation](https://github.com/immich-app/immich/assets/27055614/363df2ed-656c-4584-bd82-0708a693c5bc)
### Read-only/Editor mode on shared album
As the owner of the album, you can choose if the shared user can edit the album or to only view the content of the album without any modification.
![read-only-album](https://github.com/immich-app/immich/assets/27055614/c6f66375-b869-495a-9a86-3e87b316d109)
### Better video thumbnails
Immich now tries to find a descriptive video thumbnail instead of simply using the first frame. No more black images for thumbnails!
### Public Roadmap
We now have a [public roadmap](https://immich.app/roadmap), giving you a high-level overview of things the team is working on. The first goal of this roadmap is to bring Immich to a stable release, which is expected sometime later this year. Some of the highlights include
- Auto stacking - Auto stacking of burst photos
- Basic editor - Basic photo editing capabilities
- Workflows - Automate tasks with workflows
- Fine grained access controls - Granular access controls for users and api keys
- Better background backups - Rework background backups to be more reliable
- Private/locked photos - Private assets with extra protections
Beyond the items in the roadmap, we have _many many_ more ideas for Immich. The team and I hope that you are enjoying the application, find it helpful in your life and we have nothing but the intention of building out great software for you all!
Have an amazing Summer or Winter for those in the southern hemisphere! :D
Until next time,
Cheers!
Alex

5
docs/blog/authors.yml Normal file
View File

@@ -0,0 +1,5 @@
alextran:
name: Alex Tran
title: Maintainer of Immich
url: https://github.com/alextran1502
image_url: https://github.com/alextran1502.png

View File

@@ -1,40 +1,14 @@
# FAQ # FAQ
## Commercial Guidelines
### Are you open to commercial partnerships and collaborations?
We are working to commercialize Immich and we'd love for you to help us by making Immich better. FUTO is dedicated to developing sustainable models for developing open source software for our customers. We want our customers to be delighted by the products our engineers deliver, and we want our engineers to be paid when they succeed.
If you wish to use Immich in a commercial product not owned by FUTO, we have the following requirements:
- Plugin Integrations: Integrations for other platforms are typically approved, provided proper notification is given.
- Reseller Partnerships: Must adhere to the guidelines outlined below regarding trademark usage, and proper representation.
- Strategic Collaborations: We welcome discussions about mutually beneficial partnerships that enhance the value proposition for both organizations.
### What are your guidelines for resellers and trademark usage?
For organizations seeking to resell Immich, we have established the following guidelines to protect our brand integrity and ensure proper representation.
- We request that resellers do not display our trademarks on their websites or marketing materials. If such usage is discovered, we will contact you to request removal.
- Do not misrepresent your reseller site or services as being officially affiliated with or endorsed by Immich or our development team.
- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directy from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work.
When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app
## User ## User
### How can I reset the admin password? ### How can I reset the admin password?
The admin password can be reset by running the [reset-admin-password](/administration/server-commands.md) command on the immich-server. The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server.
### How can I see a list of all users in Immich? ### How can I see a list of all users in Immich?
You can see the list of all users by running [list-users](/administration/server-commands.md) Command on the Immich-server. You can see the list of all users by running [list-users](/docs/administration/server-commands.md) Command on the Immich-server.
--- ---
@@ -75,63 +49,33 @@ The behaviors differ based on your device manufacturer and operating system, but
On iOS (iPhone and iPad), the operating system determines if a particular app can invoke background tasks based on multiple factors, most of which the Immich app has no control over. To increase the likelihood that the background backup task is run, follow the steps below: On iOS (iPhone and iPad), the operating system determines if a particular app can invoke background tasks based on multiple factors, most of which the Immich app has no control over. To increase the likelihood that the background backup task is run, follow the steps below:
- Enable Background App Refresh for Immich in the iOS settings at `Settings > General > Background App Refresh`. - Enable Background App Refresh for Immich in the iOS settings at `Settings > General > Background App Refresh`.
- Disable **Low Power Mode** when not needed, as this can prevent apps from running in the background.
- Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich. - Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich.
- Use the Immich app more often. - Use the Immich app more often.
### Why are features in the mobile app not working with a self-signed certificate, Basic Auth, custom headers, or mutual TLS?
These network features are experimental. They often do not work with video playback, asset upload or download, and other features.
Many of these limitations are tracked in [#15230](https://github.com/immich-app/immich/issues/15230).
Instead of these experimental features, we recommend using the URL switching feature, a VPN, or a [free trusted SSL certificate](https://letsencrypt.org/) for your domain.
We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them.
Please discuss any large PRs with our dev team to ensure your time is not wasted.
### Why isn't the mobile app updated yet?
The app stores can take a few days to approve new builds of the app. If you're impatient, android APKs can be downloaded from the GitHub releases.
--- ---
## Assets ## Assets
### Does Immich change the file? ### Does Immich change the file?
No, Immich does not modify the original files. No, Immich does not touch the original file under any circumstances,
All edited metadata is saved in companion `.xmp` sidecar files and the database. all edited metadata are saved in the companion sidecar file and the database.
However, Immich will delete original files that have been trashed when the trash is emptied in the Immich UI.
### Why do my file names appear as a random string in the file manager?
When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names.
To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
It is recommended to read about [Storage Template](/administration/storage-template) before activation.
### Can I add my existing photo library? ### Can I add my existing photo library?
Yes, with an [External Library](/features/libraries.md). Yes, with an [External Library](/docs/features/libraries.md).
### What happens to existing files after I choose a new [Storage Template](/administration/storage-template.mdx)? ### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/administration/jobs-workers/#jobs) page. Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs-workers/#jobs) page.
### Why are only photos and not videos being uploaded to Immich? ### Why are only photos and not videos being uploaded to Immich?
This often happens when using a reverse proxy in front of Immich. This often happens when using a reverse proxy (such as Nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
Make sure to [set your reverse proxy](/administration/reverse-proxy/) to allow large requests.
Also, check the disk space of your reverse proxy.
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
If you are using Cloudflare Tunnel, please know that they set a maximum filesize of 100 MB that cannot be changed.
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
If you are having issues, we recommend switching to a different network deployment.
### Why are some photos stored in the file system with the wrong date? ### Why are some photos stored in the file system with the wrong date?
There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time.
The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc.,
the job may not have run automatically the first time.
### How can I hide photos from the timeline? ### How can I hide photos from the timeline?
@@ -139,11 +83,11 @@ You can _archive_ them.
### How can I backup data from Immich? ### How can I backup data from Immich?
See [Backup and Restore](/administration/backup-and-restore.md). See [Backup and Restore](/docs/administration/backup-and-restore.md).
### Does Immich support reading existing face tag metadata? ### Does Immich support reading existing face tag metadata?
Yes, it creates new faces and persons from the imported asset metadata. For details see the [feature request #4348](https://github.com/immich-app/immich/discussions/4348) and [PR #6455](https://github.com/immich-app/immich/pull/6455). No, it currently does not. There is an [open feature request on GitHub](https://github.com/immich-app/immich/discussions/4348).
### Does Immich support the filtering of NSFW images? ### Does Immich support the filtering of NSFW images?
@@ -161,8 +105,7 @@ Also, there are additional jobs for person (face) thumbnails.
### Why do files from WhatsApp not appear with the correct date? ### Why do files from WhatsApp not appear with the correct date?
Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, not the order of arrival on the device. [See #3527](https://github.com/immich-app/immich/issues/3527).
not the order of arrival on the device. [See #9116](https://github.com/immich-app/immich/discussions/9116).
### What happens if an asset exists in more than one account? ### What happens if an asset exists in more than one account?
@@ -190,42 +133,13 @@ For example, say you have existing transcodes with the policy "Videos higher tha
No. Our design principle is that the original assets should always be untouched. No. Our design principle is that the original assets should always be untouched.
### How can I mount a CIFS/Samba volume within Docker?
If you aren't able to or prefer not to mount Samba on the host (such as Windows environment), you can mount the volume within Docker.
Below is an example in the `docker-compose.yml`.
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
correlates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
```diff
...
services:
immich-server:
...
volumes:
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
- ${UPLOAD_LOCATION}:/data
- /etc/localtime:/etc/localtime:ro
+ - originals:/usr/src/app/originals
...
volumes:
model-cache:
+ originals:
+ driver_opts:
+ type: cifs
+ o: 'iocharset=utf8,username=USERNAMEHERE,password=PASSWORDHERE,rw' # change to `ro` if read only desired
+ device: '//localipaddress/sharename'
```
--- ---
## Albums ## Albums
### Can I keep my existing album structure while importing assets into Immich? ### Can I keep my existing album structure while importing assets into Immich?
Yes, by using the [Immich CLI](/features/command-line-interface) along with the `--album` flag. Yes, by using the [Immich CLI](/docs/features/command-line-interface) along with the `--album` flag.
### Is there a way to reorder photos within an album? ### Is there a way to reorder photos within an album?
@@ -243,30 +157,17 @@ We haven't implemented an official mechanism for creating albums from external l
Duplicate checking only exists for upload libraries, using the file hash. Furthermore, duplicate checking is not global, but _per library_. Therefore, a situation where the same file appears twice in the timeline is possible, especially for external libraries. Duplicate checking only exists for upload libraries, using the file hash. Furthermore, duplicate checking is not global, but _per library_. Therefore, a situation where the same file appears twice in the timeline is possible, especially for external libraries.
### Why are my edits to files not being saved in read-only external libraries?
Images in read-write external libraries (the default) can be edited as normal.
In read-only libraries (`:ro` in the `docker-compose.yml`), Immich is unable to create the `.xmp` sidecar files to store edited file metadata.
For this reason, the metadata (timestamp, location, description, star rating, etc.) cannot be edited for files in read-only external libraries.
### How are deletions of files handled in external libraries?
Immich will attempt to delete original files that have been trashed when the trash is emptied.
In read-write external libraries (the default), Immich will delete the original file.
In read-only libraries (`:ro` in the `docker-compose.yml`), files can still be trashed in the UI.
However, when the trash is emptied, the files will re-appear in the main timeline since Immich is unable to delete the original file.
--- ---
## Machine Learning ## Machine Learning
### How does smart search work? ### How does smart search work?
Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip). Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
### How does facial recognition work? ### How does facial recognition work?
See [How Facial Recognition Works](/features/facial-recognition#how-facial-recognition-works) for details. See [How Facial Recognition Works](/docs/features/facial-recognition#How-Facial-Recognition-Works) for details.
### How can I disable machine learning? ### How can I disable machine learning?
@@ -288,7 +189,7 @@ No, this is not supported. Only models listed in the [Hugging Face][huggingface]
### I want to be able to search in other languages besides English. How can I do that? ### I want to be able to search in other languages besides English. How can I do that?
You can change to a multilingual CLIP model. See [here](/features/searching#clip-models) for instructions. You can change to a multilingual CLIP model. See [here](/docs/features/smart-search#CLIP-model) for instructions.
### Does Immich support Facial Recognition for videos? ### Does Immich support Facial Recognition for videos?
@@ -299,7 +200,7 @@ Scanning the entire video for faces may be implemented in the future.
No. No.
:::tip :::tip
You can use [Smart Search](/features/searching.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results. You can use [Smart Search](/docs/features/smart-search.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results.
::: :::
### I'm getting a lot of "faces" that aren't faces, what can I do? ### I'm getting a lot of "faces" that aren't faces, what can I do?
@@ -329,7 +230,7 @@ ls clip/ facial-recognition/
### Why is Immich slow on low-memory systems like the Raspberry Pi? ### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/guides/remote-machine-learning), or [disable](/FAQ#how-can-i-disable-machine-learning) machine learning entirely. Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely.
### Can I lower CPU and RAM usage? ### Can I lower CPU and RAM usage?
@@ -339,9 +240,9 @@ The initial backup is the most intensive due to the number of jobs running. The
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2. - Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good. - Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this. - For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- At the container level, you can [set resource constraints](/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further. - At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
- It's recommended to only apply these constraints _after_ taking some of the measures here for best performance. - It's recommended to only apply these constraints _after_ taking some of the measures here for best performance.
- If these changes are not enough, see [above](/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning. - If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
### Can I limit CPU and RAM usage? ### Can I limit CPU and RAM usage?
@@ -383,7 +284,7 @@ Do not exaggerate with the job concurrency because you're probably thoroughly ov
### My server shows Server Status Offline | Version Unknown. What can I do? ### My server shows Server Status Offline | Version Unknown. What can I do?
You need to [enable WebSockets](/administration/reverse-proxy/) on your reverse proxy. You need to enable WebSockets on your reverse proxy.
--- ---
@@ -391,13 +292,7 @@ You need to [enable WebSockets](/administration/reverse-proxy/) on your reverse
### How can I see Immich logs? ### How can I see Immich logs?
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/guides/docker-help.md). Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md).
### How can I reduce the log verbosity of Redis?
To decrease Redis logs, you can add the following line to the `redis:` section of the `docker-compose.yml`:
` command: redis-server --loglevel warning`
### How can I run Immich as a non-root user? ### How can I run Immich as a non-root user?
@@ -408,13 +303,9 @@ You may need to add mount points or docker volumes for the following internal co
- `immich-machine-learning:/.cache` - `immich-machine-learning:/.cache`
- `redis:/data` - `redis:/data`
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning. The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
:::note Docker Compose Volumes For a further hardened system, you can add the following block to every container except for `immich_postgres`.
The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
:::
For a further hardened system, you can add the following block to every container.
<details> <details>
<summary>docker-compose.yml</summary> <summary>docker-compose.yml</summary>
@@ -435,7 +326,7 @@ cap_drop:
Data for Immich comes in two forms: Data for Immich comes in two forms:
1. **Metadata** stored in a Postgres database, stored in the `DB_DATA_LOCATION` folder (previously `pg_data` Docker volume). 1. **Metadata** stored in a Postgres database, stored in the `DB_DATA_LOCATION` folder (previously `pg_data` Docker volume).
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/administration/backup-and-restore#asset-types-and-storage-locations). 2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations).
:::warning :::warning
This will destroy your database and reset your instance, meaning that you start from scratch. This will destroy your database and reset your instance, meaning that you start from scratch.
@@ -463,28 +354,29 @@ If the error says the worker is exiting, then this is normal. This is a feature
There are a few reasons why this can happen. There are a few reasons why this can happen.
If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory. If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory. Consider either increasing the server's RAM or moving the service to a server with more RAM.
Consider either increasing the server's RAM or moving the service to a server with more RAM.
If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible with Immich. If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible. This is unlikely to occur on version 1.92.0 or later. Consider upgrading if your version of Immich is below that.
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
## Database ## Database
### Why am I getting database ownership errors? ### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem. If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/install/requirements#special-requirements-for-windows-users) for more details. NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details.
### How can I verify the integrity of my database? ### How can I verify the integrity of my database?
Database checksums are enabled by default for new installations since v1.104.0. You can check if they are enabled by running the following command. If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command.
A result of `on` means that checksums are enabled. A result of `on` means that checksums are enabled.
<details> <details>
<summary>Check if checksums are enabled</summary> <summary>Check if checksums are enabled</summary>
```bash ```bash
docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME> --command="show data_checksums" docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="show data_checksums"
data_checksums data_checksums
---------------- ----------------
on on
@@ -493,13 +385,13 @@ docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME>
</details> </details>
If checksums are enabled, you can check the status of the database with the following command. A normal result is all `0`s. If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes.
<details> <details>
<summary>Check for database corruption</summary> <summary>Check for database corruption</summary>
```bash ```bash
docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL" docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
datname | checksum_failures | checksum_last_failure datname | checksum_failures | checksum_last_failure
-----------+-------------------+----------------------- -----------+-------------------+-----------------------
postgres | 0 | postgres | 0 |
@@ -511,24 +403,6 @@ docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME>
</details> </details>
You can also scan the Postgres database file structure for errors:
<details>
<summary>Scan for file structure errors</summary>
```bash
docker exec -it immich_postgres pg_amcheck --username=<DB_USERNAME> --heapallindexed --parent-check --rootdescend --progress --all --install-missing
```
A normal result will end something like this and return with an exit code of `0`:
```bash
7470/8832 relations (84%), 730829/734735 pages (99%)
8425/8832 relations (95%), 734367/734735 pages (99%)
8832/8832 relations (100%), 734735/734735 pages (100%)
```
</details>
If corruption is detected, you should immediately make a backup before performing any other work in the database. If corruption is detected, you should immediately make a backup before performing any other work in the database.
To do so, you may need to set the `zero_damaged_pages=on` flag for the database server to allow `pg_dumpall` to succeed. To do so, you may need to set the `zero_damaged_pages=on` flag for the database server to allow `pg_dumpall` to succeed.
After taking a backup, the recommended next step is to restore the database from a healthy backup before corruption was detected. After taking a backup, the recommended next step is to restore the database from a healthy backup before corruption was detected.

View File

@@ -3,11 +3,7 @@
import Tabs from '@theme/Tabs'; import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem'; import TabItem from '@theme/TabItem';
A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/guides/template-backup-script.md) A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/docs/guides/template-backup-script.md)
:::danger
The instructions on this page show you how to prepare your Immich instance to be backed up, and which files to take a backup of. You still need to take care of using an actual backup tool to make a backup yourself.
:::
## Database ## Database
@@ -19,104 +15,107 @@ Immich saves [file paths in the database](https://github.com/immich-app/immich/d
Refer to the official [postgres documentation](https://www.postgresql.org/docs/current/backup.html) for details about backing up and restoring a postgres database. Refer to the official [postgres documentation](https://www.postgresql.org/docs/current/backup.html) for details about backing up and restoring a postgres database.
::: :::
The recommended way to backup and restore the Immich database is to use the `pg_dumpall` command. When restoring, you need to delete the `DB_DATA_LOCATION` folder (if it exists) to reset the database.
:::caution :::caution
It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored. It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
::: :::
### Automatic Database Dumps
:::warning
The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files.
There is no monitoring for these dumps and you will not be notified if they are unsuccessful.
:::
:::caution
The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below.
:::
For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`.
Please be sure to make your own, independent backup of the database together with the asset folders as noted below.
You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup).
By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM.
#### Trigger Dump
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/jobs-status).
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
This dumps will count towards the last `X` dumps that will be kept based on your settings.
#### Restoring
We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host.
Then please follow the steps in the following section for restoring the database.
### Manual Backup and Restore
<Tabs> <Tabs>
<TabItem value="Linux system" label="Linux system" default> <TabItem value="Linux system" label="Linux system" default>
```bash title='Backup' ```bash title='Backup'
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME> | gzip > "/path/to/backup/dump.sql.gz" docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | gzip > "/path/to/backup/dump.sql.gz"
``` ```
```bash title='Restore' ```bash title='Restore'
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch docker compose down -v # CAUTION! Deletes all Immich data to start from scratch.
## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database # rm -rf DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch.
# rm -rf DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch docker compose pull # Update to latest version of Immich (if desired)
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 create # Create Docker containers for Immich apps without running them
docker start immich_postgres # Start Postgres server docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up sleep 10 # Wait for Postgres server to start up
# Check the database user if you deviated from the default gunzip < "/path/to/backup/dump.sql.gz" \
gunzip --stdout "/path/to/backup/dump.sql.gz" \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup | docker exec -i immich_postgres psql --username=postgres # Restore Backup
docker compose up -d # Start remainder of Immich apps docker compose up -d # Start remainder of Immich apps
``` ```
</TabItem> </TabItem>
<TabItem value="Windows system (PowerShell)" label="Windows system (PowerShell)"> <TabItem value="Windows system (PowerShell)" label="Windows system (PowerShell)">
```powershell title='Backup' ```powershell title='Backup'
[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=<DB_USERNAME>)) docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "\path\to\backup\dump.sql"
``` ```
```powershell title='Restore' ```powershell title='Restore'
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch docker compose down -v # CAUTION! Deletes all Immich data to start from scratch.
## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database # Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch.
# Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch docker compose pull # Update to latest version of Immich (if desired)
## You should mount the backup (as a volume, example: `- 'C:\path\to\backup\dump.sql:/dump.sql'`) into the immich_postgres container using the docker-compose.yml docker compose create # Create Docker containers for Immich apps without running them.
docker compose pull # Update to latest version of Immich (if desired) docker start immich_postgres # Start Postgres server
docker compose create # Create Docker containers for Immich apps without running them sleep 10 # Wait for Postgres server to start up
docker start immich_postgres # Start Postgres server gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
sleep 10 # Wait for Postgres server to start up docker compose up -d # Start remainder of Immich apps
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip --stdout`
cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
exit # Exit the Docker shell
docker compose up -d # Start remainder of Immich apps
``` ```
</TabItem> </TabItem>
</Tabs> </Tabs>
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.), in which case you need to delete the `DB_DATA_LOCATION` folder to reset the database. 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.).
:::tip :::tip
Some deployment methods make it difficult to start the database without also starting the server. In these cases, you may set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Be sure to remove this variable and restart the services after the database is restored. Some deployment methods make it difficult to start the database without also starting the server or microservices. In these cases, you may set the environmental variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Note that both the server and microservices must have this variable set to prevent the migrations from running. Be sure to remove this variable and restart the services after the database is restored.
:::
The database dumps can also be automated (using [this image](https://github.com/prodrigestivill/docker-postgres-backup-local)) by editing the docker compose file to match the following:
```yaml
services:
...
backup:
container_name: immich_db_dumper
image: prodrigestivill/postgres-backup-local:14
restart: always
env_file:
- .env
environment:
POSTGRES_HOST: database
POSTGRES_CLUSTER: 'TRUE'
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE_NAME}
SCHEDULE: "@daily"
POSTGRES_EXTRA_OPTS: '--clean --if-exists'
BACKUP_DIR: /db_dumps
volumes:
- ./db_dumps:/db_dumps
depends_on:
- database
```
Then you can restore with the same command but pointed at the latest dump.
```bash title='Automated Restore'
gunzip < db_dumps/last/immich-latest.sql.gz \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| docker exec -i immich_postgres psql --username=postgres
```
:::note
If you see the error `ERROR: type "earth" does not exist`, or you have problems with Reverse Geocoding after a restore, add the following `sed` fragment to your restore command.
Example: `gunzip < "/path/to/backup/dump.sql.gz" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | docker exec -i immich_postgres psql --username=postgres`
::: :::
## Filesystem ## Filesystem
Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders: Immich stores two types of content in the filesystem: (1) original, unmodified assets (photos and videos), and (2) generated content. Only the original content needs to be backed-up, which is stored in the following folders:
1. `UPLOAD_LOCATION/library` 1. `UPLOAD_LOCATION/library`
2. `UPLOAD_LOCATION/upload` 2. `UPLOAD_LOCATION/upload`
3. `UPLOAD_LOCATION/profile` 3. `UPLOAD_LOCATION/profile`
If you choose to back up only those folders, you will need to rerun the transcoding and thumbnail generation jobs for all assets after you restore from a backup.
:::caution :::caution
If you moved some of these folders onto a different storage device, such as `profile/`, make sure to adjust the backup path to match your setup If you moved some of these folders onto a different storage device, such as `profile/`, make sure to adjust the backup path to match your setup
::: :::
@@ -150,17 +149,19 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces. - Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`. - Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
- **Encoded Assets:** - **Encoded Assets:**
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed. - Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`. - Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
- **Postgres** - **Postgres**
- The Immich database containing all the information to allow the system to function properly. - The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version. **Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`. - Stored in `UPLOAD_LOCATION/postgres`.
:::danger :::danger
A backup of this folder does not constitute a backup of your database! A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup. Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup.
::: :::
</TabItem> </TabItem>
@@ -190,7 +191,7 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
- Stored in `UPLOAD_LOCATION/profile/<userID>`. - Stored in `UPLOAD_LOCATION/profile/<userID>`.
- **Thumbs Images:** - **Thumbs Images:**
- Preview images (blurred, small, large) for each asset and thumbnails for recognized faces. - Preview images (blurred, small, large) for each asset and thumbnails for recognized faces.
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`. - Stored in `UPLOCAD_LOCATION/thumbs/<userID>`.
- **Encoded Assets:** - **Encoded Assets:**
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed. - Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`. - Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
@@ -199,13 +200,14 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`. - Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload. - Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
- **Postgres** - **Postgres**
- The Immich database containing all the information to allow the system to function properly. - The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version. **Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `DB_DATA_LOCATION`. - Stored in `UPLOAD_LOCATION/postgres`.
:::danger :::danger
A backup of this folder does not constitute a backup of your database! A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup. Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup.
::: :::
</TabItem> </TabItem>
@@ -216,10 +218,3 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files. Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface. You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
::: :::
## Backup ordering
A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore.
The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync.
If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets.

View File

@@ -8,20 +8,16 @@ Immich supports the option to send notifications via Email for the following eve
## SMTP settings ## SMTP settings
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`. You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
Under Email, enter the required details to connect with an SMTP server. Under Email, enter the following details to connect with SMTP servers.
You can use [this guide](/guides/smtp-gmail) to use Gmail's SMTP server. You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
## User's notifications settings ## User's notifications settings
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events: Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
<img src={require('./img/user-notifications-settings.webp').default} width="80%" title="User notification settings" /> <img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />
## Notification templates
You can override the default notification text with custom templates in HTML format. You can use tags to show dynamic tags in your templates.
<img src={require('./img/user-notifications-templates.webp').default} width="80%" title="User notification templates" />

Some files were not shown because too many files have changed in this diff Show More