mirror of
https://github.com/immich-app/immich.git
synced 2025-12-13 08:10:42 -08:00
Compare commits
2 Commits
dev/recogn
...
feat/backg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a156b813e | ||
|
|
4907916345 |
2
.devcontainer/.gitignore
vendored
2
.devcontainer/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
.env
|
|
||||||
library
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:a20b8a3538313487ac9266875bbf733e544c1aa2091df2bb99ab592a6d4f7399
|
|
||||||
FROM ${BASEIMAGE}
|
|
||||||
|
|
||||||
# Flutter SDK
|
|
||||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
|
||||||
ENV FLUTTER_CHANNEL="stable"
|
|
||||||
ENV FLUTTER_VERSION="3.29.1"
|
|
||||||
ENV FLUTTER_HOME=/flutter
|
|
||||||
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
|
||||||
|
|
||||||
# Flutter SDK
|
|
||||||
RUN mkdir -p ${FLUTTER_HOME} \
|
|
||||||
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
|
|
||||||
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
|
|
||||||
&& rm flutter.tar.xz \
|
|
||||||
&& chown -R 1000:1000 ${FLUTTER_HOME}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Immich",
|
|
||||||
"service": "immich-devcontainer",
|
|
||||||
"dockerComposeFile": [
|
|
||||||
"docker-compose.yml",
|
|
||||||
"../docker/docker-compose.dev.yml"
|
|
||||||
],
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"Dart-Code.dart-code",
|
|
||||||
"Dart-Code.flutter",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"dcmdev.dcm-vscode-extension",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"svelte.svelte-vscode"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwardPorts": [],
|
|
||||||
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
|
|
||||||
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
|
|
||||||
"overrideCommand": true,
|
|
||||||
"workspaceFolder": "/immich",
|
|
||||||
"remoteUser": "node"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
services:
|
|
||||||
immich-devcontainer:
|
|
||||||
build:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
extra_hosts:
|
|
||||||
- 'host.docker.internal:host-gateway'
|
|
||||||
volumes:
|
|
||||||
- ..:/immich:cached
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# If .env file does not exist, create it by copying example.env from the docker folder
|
|
||||||
if [ ! -f ".devcontainer/.env" ]; then
|
|
||||||
cp docker/example.env .devcontainer/.env
|
|
||||||
fi
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Enable multiarch for arm64 if necessary
|
|
||||||
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
|
|
||||||
sudo dpkg --add-architecture amd64 && \
|
|
||||||
sudo apt-get update && \
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
qemu-user-static \
|
|
||||||
libc6:amd64 \
|
|
||||||
libstdc++6:amd64 \
|
|
||||||
libgcc1:amd64
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install DCM
|
|
||||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
|
||||||
sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
|
||||||
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install dcm
|
|
||||||
|
|
||||||
dart --disable-analytics
|
|
||||||
|
|
||||||
# Install immich
|
|
||||||
cd /immich || exit
|
|
||||||
make install-all
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
!docker/scripts
|
|
||||||
docs/
|
docs/
|
||||||
e2e/
|
e2e/
|
||||||
fastlane/
|
fastlane/
|
||||||
@@ -22,7 +21,6 @@ open-api/typescript-sdk/node_modules/
|
|||||||
server/coverage/
|
server/coverage/
|
||||||
server/node_modules/
|
server/node_modules/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/src/queries
|
|
||||||
server/dist/
|
server/dist/
|
||||||
server/www/
|
server/www/
|
||||||
|
|
||||||
@@ -30,4 +28,3 @@ web/node_modules/
|
|||||||
web/coverage/
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
web/build/
|
||||||
web/.env
|
|
||||||
|
|||||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -6,9 +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
|
|
||||||
|
|
||||||
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
1
.github/.nvmrc
vendored
@@ -1 +0,0 @@
|
|||||||
22.14.0
|
|
||||||
12
.github/DISCUSSION_TEMPLATE/feature-request.yaml
vendored
12
.github/DISCUSSION_TEMPLATE/feature-request.yaml
vendored
@@ -1,19 +1,17 @@
|
|||||||
title: '[Feature] feature-name-goes-here'
|
title: "[Feature] <feature-name-goes-here>"
|
||||||
labels: ['feature']
|
labels: ["feature"]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Please use this form to request new feature for Immich.
|
Please use this form to request new feature for Immich
|
||||||
Stick to only a single feature per request. If you list multiple different features at once,
|
|
||||||
your request will be closed.
|
|
||||||
|
|
||||||
- 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
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
custom: ['https://buy.immich.app', 'https://immich.store']
|
|
||||||
15
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
15
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -1,13 +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'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
@@ -84,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
|
||||||
|
|
||||||
@@ -97,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
|
||||||
|
|||||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.gg/D8JsnBEuKb
|
||||||
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.gg/D8JsnBEuKb
|
||||||
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!
|
||||||
|
|||||||
1
.github/PULL_REQUEST_TEMPLATE/config.yml
vendored
1
.github/PULL_REQUEST_TEMPLATE/config.yml
vendored
@@ -1 +1,2 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
blank_pull_request_template_enabled: false
|
blank_pull_request_template_enabled: false
|
||||||
|
|||||||
22
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
22
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal 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
|
||||||
118
.github/actions/image-build/action.yml
vendored
118
.github/actions/image-build/action.yml
vendored
@@ -1,118 +0,0 @@
|
|||||||
name: 'Single arch image build'
|
|
||||||
description: 'Build single-arch image on platform appropriate runner'
|
|
||||||
inputs:
|
|
||||||
image:
|
|
||||||
description: 'Name of the image to build'
|
|
||||||
required: true
|
|
||||||
ghcr-token:
|
|
||||||
description: 'GitHub Container Registry token'
|
|
||||||
required: true
|
|
||||||
platform:
|
|
||||||
description: 'Platform to build for'
|
|
||||||
required: true
|
|
||||||
artifact-key-base:
|
|
||||||
description: 'Base key for artifact name'
|
|
||||||
required: true
|
|
||||||
context:
|
|
||||||
description: 'Path to build context'
|
|
||||||
required: true
|
|
||||||
dockerfile:
|
|
||||||
description: 'Path to Dockerfile'
|
|
||||||
required: true
|
|
||||||
build-args:
|
|
||||||
description: 'Docker build arguments'
|
|
||||||
required: false
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
- name: Prepare
|
|
||||||
id: prepare
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
PLATFORM: ${{ inputs.platform }}
|
|
||||||
run: |
|
|
||||||
echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ inputs.ghcr-token }}
|
|
||||||
|
|
||||||
- name: Generate cache key suffix
|
|
||||||
id: cache-key-suffix
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
REF: ${{ github.ref_name }}
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
||||||
echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
|
|
||||||
echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate cache target
|
|
||||||
id: cache-target
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
BUILD_ARGS: ${{ inputs.build-args }}
|
|
||||||
IMAGE: ${{ inputs.image }}
|
|
||||||
SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }}
|
|
||||||
PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }}
|
|
||||||
run: |
|
|
||||||
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
|
||||||
CACHE_KEY="${PLATFORM_PAIR}-${HASH}"
|
|
||||||
echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT
|
|
||||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
|
||||||
# Essentially just ignore the cache output (forks 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,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate docker image tags
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
|
||||||
env:
|
|
||||||
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
|
||||||
|
|
||||||
- name: Build and push image
|
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
|
||||||
with:
|
|
||||||
context: ${{ inputs.context }}
|
|
||||||
file: ${{ inputs.dockerfile }}
|
|
||||||
platforms: ${{ inputs.platform }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
|
||||||
cache-from: |
|
|
||||||
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ steps.cache-key-suffix.outputs.suffix }}
|
|
||||||
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main
|
|
||||||
outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
build-args: |
|
|
||||||
BUILD_ID=${{ github.run_id }}
|
|
||||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }}
|
|
||||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
|
||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
|
||||||
${{ inputs.build-args }}
|
|
||||||
|
|
||||||
- name: Export digest
|
|
||||||
shell: bash
|
|
||||||
run: | # zizmor: ignore[template-injection]
|
|
||||||
mkdir -p ${{ runner.temp }}/digests
|
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
|
||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
|
||||||
|
|
||||||
- name: Upload digest
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }}
|
|
||||||
path: ${{ runner.temp }}/digests/*
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 1
|
|
||||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
38
.github/labeler.yml
vendored
38
.github/labeler.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
cli:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- cli/src/**
|
|
||||||
|
|
||||||
documentation:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- docs/blob/**
|
|
||||||
- docs/docs/**
|
|
||||||
- docs/src/**
|
|
||||||
- docs/static/**
|
|
||||||
|
|
||||||
🖥️web:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- web/src/**
|
|
||||||
- web/static/**
|
|
||||||
|
|
||||||
📱mobile:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- mobile/lib/**
|
|
||||||
- mobile/test/**
|
|
||||||
|
|
||||||
🗄️server:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- server/src/**
|
|
||||||
- server/test/**
|
|
||||||
|
|
||||||
🧠machine-learning:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- machine-learning/app/**
|
|
||||||
|
|
||||||
changelog:translation:
|
|
||||||
- head-branch: ['^chore/translations$']
|
|
||||||
28
.github/package-lock.json
generated
vendored
28
.github/package-lock.json
generated
vendored
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": ".github",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier": "^3.5.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "3.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
|
||||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin/prettier.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
.github/package.json
vendored
9
.github/package.json
vendored
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"format": "prettier --check .",
|
|
||||||
"format:fix": "prettier --write ."
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier": "^3.5.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
.github/pull_request_template.md
vendored
36
.github/pull_request_template.md
vendored
@@ -1,36 +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/`)
|
|
||||||
74
.github/release.yml
vendored
74
.github/release.yml
vendored
@@ -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:
|
||||||
|
- "*"
|
||||||
|
|||||||
67
.github/workflows/build-mobile.yml
vendored
67
.github/workflows/build-mobile.yml
vendored
@@ -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,59 +15,37 @@ 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.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- id: found_paths
|
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
mobile:
|
|
||||||
- 'mobile/**'
|
|
||||||
workflow:
|
|
||||||
- '.github/workflows/build-mobile.yml'
|
|
||||||
- name: Check if we should force jobs to run
|
|
||||||
id: should_force
|
|
||||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
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]' && needs.pre-job.outputs.should_run == 'true' }}
|
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- name: Determine ref
|
||||||
with:
|
id: get-ref
|
||||||
ref: ${{ inputs.ref || github.sha }}
|
run: |
|
||||||
persist-credentials: false
|
input_ref="${{ inputs.ref }}"
|
||||||
|
github_ref="${{ github.sha }}"
|
||||||
|
ref="${input_ref:-$github_ref}"
|
||||||
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ steps.get-ref.outputs.ref }}
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version: '3.22.0'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
@@ -89,10 +58,6 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Generate translation file
|
|
||||||
run: make translation
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Build Android App Bundle
|
- name: Build Android App Bundle
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
env:
|
env:
|
||||||
@@ -104,7 +69,7 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- name: Publish Android Artifact
|
- name: Publish Android Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
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
|
||||||
|
|||||||
19
.github/workflows/cache-cleanup.yml
vendored
19
.github/workflows/cache-cleanup.yml
vendored
@@ -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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
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 }}
|
||||||
|
|||||||
55
.github/workflows/cli.yml
vendored
55
.github/workflows/cli.yml
vendored
@@ -1,43 +1,39 @@
|
|||||||
name: CLI Build
|
name: CLI Build
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- '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"
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
concurrency:
|
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version: "20.x"
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: "https://registry.npmjs.org"
|
||||||
- name: Prepare SDK
|
- name: Prepare SDK
|
||||||
run: npm ci --prefix ../open-api/typescript-sdk/
|
run: npm ci --prefix ../open-api/typescript-sdk/
|
||||||
- name: Build SDK
|
- name: Build SDK
|
||||||
@@ -45,32 +41,27 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm publish
|
- run: npm publish
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
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.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@v3.3.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
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
|
||||||
@@ -85,22 +76,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
images: |
|
images: |
|
||||||
name=ghcr.io/${{ github.repository_owner }}/immich-cli
|
name=ghcr.io/${{ github.repository_owner }}/immich-cli
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
|
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
uses: docker/build-push-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
|||||||
67
.github/workflows/codeql-analysis.yml
vendored
67
.github/workflows/codeql-analysis.yml
vendored
@@ -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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
|
||||||
|
|
||||||
# ℹ️ 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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
# 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
73
.github/workflows/docker-cleanup.yml
vendored
Normal 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.6.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.6.0
|
||||||
|
with:
|
||||||
|
token: "${{ env.TOKEN }}"
|
||||||
|
owner: "immich-app"
|
||||||
|
do_delete: "true"
|
||||||
|
is_org: "true"
|
||||||
|
package_name: "${{ matrix.primary-name }}"
|
||||||
275
.github/workflows/docker.yml
vendored
275
.github/workflows/docker.yml
vendored
@@ -5,6 +5,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
@@ -12,190 +13,118 @@ 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_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- id: found_paths
|
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
server:
|
|
||||||
- 'server/**'
|
|
||||||
- 'openapi/**'
|
|
||||||
- 'web/**'
|
|
||||||
- 'i18n/**'
|
|
||||||
machine-learning:
|
|
||||||
- 'machine-learning/**'
|
|
||||||
workflow:
|
|
||||||
- '.github/workflows/docker.yml'
|
|
||||||
- '.github/workflows/multi-runner-build.yml'
|
|
||||||
- '.github/actions/image-build'
|
|
||||||
|
|
||||||
- name: Check if we should force jobs to run
|
|
||||||
id: should_force
|
|
||||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
retag_ml:
|
|
||||||
name: Re-Tag ML
|
|
||||||
needs: pre-job
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == '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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
|
||||||
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: ${{ 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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
|
||||||
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: ${{ needs.pre-job.outputs.should_run_ml == '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: ./.github/workflows/multi-runner-build.yml
|
|
||||||
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: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
platforms: linux/amd64,linux/arm64
|
||||||
uses: ./.github/workflows/multi-runner-build.yml
|
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:
|
||||||
- name: Any jobs failed?
|
- name: Checkout
|
||||||
if: ${{ contains(needs.*.result, 'failure') }}
|
uses: actions/checkout@v4
|
||||||
run: exit 1
|
|
||||||
- name: All jobs passed or skipped
|
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
|
||||||
# zizmor: ignore[template-injection]
|
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
|
||||||
|
|
||||||
success-check-ml:
|
- name: Set up QEMU
|
||||||
name: Docker Build & Push ML Success
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
needs: [machine-learning, retag_ml]
|
|
||||||
permissions: {}
|
- name: Set up Docker Buildx
|
||||||
runs-on: ubuntu-latest
|
uses: docker/setup-buildx-action@v3.3.0
|
||||||
if: always()
|
|
||||||
steps:
|
- name: Login to Docker Hub
|
||||||
- name: Any jobs failed?
|
# Only push to Docker Hub when making a release
|
||||||
if: ${{ contains(needs.*.result, 'failure') }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
run: exit 1
|
uses: docker/login-action@v3
|
||||||
- name: All jobs passed or skipped
|
with:
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
# zizmor: ignore[template-injection]
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
|
||||||
|
- 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@v5.3.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 }}
|
||||||
|
build-args: |
|
||||||
|
DEVICE=${{ matrix.device }}
|
||||||
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
|||||||
76
.github/workflows/docs-build.yml
vendored
76
.github/workflows/docs-build.yml
vendored
@@ -1,76 +0,0 @@
|
|||||||
name: Docs build
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-job:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
outputs:
|
|
||||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- id: found_paths
|
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
docs:
|
|
||||||
- 'docs/**'
|
|
||||||
workflow:
|
|
||||||
- '.github/workflows/docs-build.yml'
|
|
||||||
- name: Check if we should force jobs to run
|
|
||||||
id: should_force
|
|
||||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Docs Build
|
|
||||||
needs: pre-job
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./docs
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './docs/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run npm install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: npm run format
|
|
||||||
|
|
||||||
- name: Run build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Upload build output
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: docs-build-output
|
|
||||||
path: docs/build/
|
|
||||||
include-hidden-files: true
|
|
||||||
retention-days: 1
|
|
||||||
216
.github/workflows/docs-deploy.yml
vendored
216
.github/workflows/docs-deploy.yml
vendored
@@ -1,216 +0,0 @@
|
|||||||
name: Docs deploy
|
|
||||||
on:
|
|
||||||
workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
|
||||||
workflows: ['Docs build']
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
checks:
|
|
||||||
name: Docs Deploy Checks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: read
|
|
||||||
outputs:
|
|
||||||
parameters: ${{ steps.parameters.outputs.result }}
|
|
||||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
|
||||||
steps:
|
|
||||||
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
|
|
||||||
run: echo 'The triggering workflow did not succeed' && exit 1
|
|
||||||
- name: Get artifact
|
|
||||||
id: get-artifact
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
||||||
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
|
|
||||||
id: parameters
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
||||||
env:
|
|
||||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const eventType = context.payload.workflow_run.event;
|
|
||||||
const isFork = context.payload.workflow_run.repository.fork;
|
|
||||||
|
|
||||||
let parameters;
|
|
||||||
|
|
||||||
console.log({eventType, isFork});
|
|
||||||
|
|
||||||
if (eventType == "push") {
|
|
||||||
const branch = context.payload.workflow_run.head_branch;
|
|
||||||
console.log({branch});
|
|
||||||
const shouldDeploy = !isFork && branch == "main";
|
|
||||||
parameters = {
|
|
||||||
event: "branch",
|
|
||||||
name: "main",
|
|
||||||
shouldDeploy
|
|
||||||
};
|
|
||||||
} else if (eventType == "pull_request") {
|
|
||||||
let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
|
|
||||||
if(!pull_number) {
|
|
||||||
const {HEAD_SHA} = process.env;
|
|
||||||
const response = await github.rest.search.issuesAndPullRequests({q: `repo:${{ github.repository }} is:pr sha:${HEAD_SHA}`,per_page: 1,})
|
|
||||||
const items = response.data.items
|
|
||||||
if (items.length < 1) {
|
|
||||||
throw new Error("No pull request found for the commit")
|
|
||||||
}
|
|
||||||
const pullRequestNumber = items[0].number
|
|
||||||
console.info("Pull request number is", pullRequestNumber)
|
|
||||||
pull_number = pullRequestNumber
|
|
||||||
}
|
|
||||||
const {data: pr} = await github.rest.pulls.get({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log({pull_number});
|
|
||||||
|
|
||||||
parameters = {
|
|
||||||
event: "pr",
|
|
||||||
name: `pr-${pull_number}`,
|
|
||||||
pr_number: pull_number,
|
|
||||||
shouldDeploy: true
|
|
||||||
};
|
|
||||||
} else if (eventType == "release") {
|
|
||||||
parameters = {
|
|
||||||
event: "release",
|
|
||||||
name: context.payload.workflow_run.head_branch,
|
|
||||||
shouldDeploy: !isFork
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(parameters);
|
|
||||||
return parameters;
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Docs Deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: checks
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Load parameters
|
|
||||||
id: parameters
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
||||||
env:
|
|
||||||
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const parameters = JSON.parse(process.env.PARAM_JSON);
|
|
||||||
core.setOutput("event", parameters.event);
|
|
||||||
core.setOutput("name", parameters.name);
|
|
||||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
|
||||||
|
|
||||||
- name: Download artifact
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
||||||
env:
|
|
||||||
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
let artifact = JSON.parse(process.env.ARTIFACT_JSON);
|
|
||||||
let download = await github.rest.actions.downloadArtifact({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
artifact_id: artifact.id,
|
|
||||||
archive_format: 'zip',
|
|
||||||
});
|
|
||||||
let fs = require('fs');
|
|
||||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs-build-output.zip`, Buffer.from(download.data));
|
|
||||||
|
|
||||||
- name: Unzip artifact
|
|
||||||
run: unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build"
|
|
||||||
|
|
||||||
- name: Deploy Docs Subdomain
|
|
||||||
env:
|
|
||||||
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
|
|
||||||
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
|
||||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
|
||||||
with:
|
|
||||||
tg_version: '0.58.12'
|
|
||||||
tofu_version: '1.7.1'
|
|
||||||
tg_dir: 'deployment/modules/cloudflare/docs'
|
|
||||||
tg_command: 'apply'
|
|
||||||
|
|
||||||
- name: Deploy Docs Subdomain Output
|
|
||||||
id: docs-output
|
|
||||||
env:
|
|
||||||
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
|
|
||||||
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
|
||||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
|
||||||
with:
|
|
||||||
tg_version: '0.58.12'
|
|
||||||
tofu_version: '1.7.1'
|
|
||||||
tg_dir: 'deployment/modules/cloudflare/docs'
|
|
||||||
tg_command: 'output -json'
|
|
||||||
|
|
||||||
- name: Output Cleaning
|
|
||||||
id: clean
|
|
||||||
env:
|
|
||||||
TG_OUTPUT: ${{ steps.docs-output.outputs.tg_action_output }}
|
|
||||||
run: |
|
|
||||||
CLEANED=$(echo "$TG_OUTPUT" | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
|
|
||||||
echo "output=$CLEANED" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages
|
|
||||||
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
|
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }}
|
|
||||||
workingDirectory: 'docs'
|
|
||||||
directory: 'build'
|
|
||||||
branch: ${{ steps.parameters.outputs.name }}
|
|
||||||
wranglerVersion: '3'
|
|
||||||
|
|
||||||
- name: Deploy Docs Release Domain
|
|
||||||
if: ${{ steps.parameters.outputs.event == 'release' }}
|
|
||||||
env:
|
|
||||||
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
|
||||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
|
||||||
with:
|
|
||||||
tg_version: '0.58.12'
|
|
||||||
tofu_version: '1.7.1'
|
|
||||||
tg_dir: 'deployment/modules/cloudflare/docs-release'
|
|
||||||
tg_command: 'apply'
|
|
||||||
|
|
||||||
- name: Comment
|
|
||||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
|
|
||||||
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
|
||||||
with:
|
|
||||||
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
|
|
||||||
body: |
|
|
||||||
📖 Documentation deployed to [${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }}](https://${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }})
|
|
||||||
emojis: 'rocket'
|
|
||||||
body-include: '<!-- Docs PR URL -->'
|
|
||||||
40
.github/workflows/docs-destroy.yml
vendored
40
.github/workflows/docs-destroy.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Docs destroy
|
|
||||||
on:
|
|
||||||
pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: Docs Destroy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Destroy Docs Subdomain
|
|
||||||
env:
|
|
||||||
TF_VAR_prefix_name: 'pr-${{ github.event.number }}'
|
|
||||||
TF_VAR_prefix_event_type: 'pr'
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
|
||||||
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
|
||||||
with:
|
|
||||||
tg_version: '0.58.12'
|
|
||||||
tofu_version: '1.7.1'
|
|
||||||
tg_dir: 'deployment/modules/cloudflare/docs'
|
|
||||||
tg_command: 'destroy -refresh=false'
|
|
||||||
|
|
||||||
- name: Comment
|
|
||||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
|
|
||||||
with:
|
|
||||||
number: ${{ github.event.number }}
|
|
||||||
delete: true
|
|
||||||
body-include: '<!-- Docs PR URL -->'
|
|
||||||
55
.github/workflows/fix-format.yml
vendored
55
.github/workflows/fix-format.yml
vendored
@@ -1,55 +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@df432ceedc7162793a195dd1713ff69aefc7379e # v2
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
|
||||||
|
|
||||||
- name: 'Checkout'
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
|
||||||
persist-credentials: true
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './server/.nvmrc'
|
|
||||||
|
|
||||||
- name: Fix formatting
|
|
||||||
run: make install-all && make format-all
|
|
||||||
|
|
||||||
- name: Commit and push
|
|
||||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9
|
|
||||||
with:
|
|
||||||
default_author: github_actions
|
|
||||||
message: 'chore: fix formatting'
|
|
||||||
|
|
||||||
- name: Remove label
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
||||||
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'
|
|
||||||
})
|
|
||||||
185
.github/workflows/multi-runner-build.yml
vendored
185
.github/workflows/multi-runner-build.yml
vendored
@@ -1,185 +0,0 @@
|
|||||||
name: 'Multi-runner container image build'
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
image:
|
|
||||||
description: 'Name of the image'
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
context:
|
|
||||||
description: 'Path to build context'
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
dockerfile:
|
|
||||||
description: 'Path to Dockerfile'
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
tag-suffix:
|
|
||||||
description: 'Suffix to append to the image tag'
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
dockerhub-push:
|
|
||||||
description: 'Push to Docker Hub'
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
build-args:
|
|
||||||
description: 'Docker build arguments'
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
platforms:
|
|
||||||
description: 'Platforms to build for'
|
|
||||||
type: string
|
|
||||||
runner-mapping:
|
|
||||||
description: 'Mapping from platforms to runners'
|
|
||||||
type: string
|
|
||||||
secrets:
|
|
||||||
DOCKERHUB_USERNAME:
|
|
||||||
required: false
|
|
||||||
DOCKERHUB_TOKEN:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}
|
|
||||||
DOCKERHUB_IMAGE: altran1502/${{ inputs.image }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
matrix:
|
|
||||||
name: 'Generate matrix'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
matrix: ${{ steps.matrix.outputs.matrix }}
|
|
||||||
key: ${{ steps.artifact-key.outputs.base }}
|
|
||||||
steps:
|
|
||||||
- name: Generate build matrix
|
|
||||||
id: matrix
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }}
|
|
||||||
RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }}
|
|
||||||
run: |
|
|
||||||
matrix=$(jq -R -c \
|
|
||||||
--argjson runner_mapping "${RUNNER_MAPPING}" \
|
|
||||||
'split(",") | map({platform: ., runner: $runner_mapping[.]})' \
|
|
||||||
<<< "${PLATFORMS}")
|
|
||||||
echo "${matrix}"
|
|
||||||
echo "matrix=${matrix}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Determine artifact key
|
|
||||||
id: artifact-key
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
IMAGE: ${{ inputs.image }}
|
|
||||||
SUFFIX: ${{ inputs.tag-suffix }}
|
|
||||||
run: |
|
|
||||||
if [[ -n "${SUFFIX}" ]]; then
|
|
||||||
base="${IMAGE}${SUFFIX}-digests"
|
|
||||||
else
|
|
||||||
base="${IMAGE}-digests"
|
|
||||||
fi
|
|
||||||
echo "${base}"
|
|
||||||
echo "base=${base}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build:
|
|
||||||
needs: matrix
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include: ${{ fromJson(needs.matrix.outputs.matrix) }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: ./.github/actions/image-build
|
|
||||||
with:
|
|
||||||
context: ${{ inputs.context }}
|
|
||||||
dockerfile: ${{ inputs.dockerfile }}
|
|
||||||
image: ${{ env.GHCR_IMAGE }}
|
|
||||||
ghcr-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
platform: ${{ matrix.platform }}
|
|
||||||
artifact-key-base: ${{ needs.matrix.outputs.key }}
|
|
||||||
build-args: ${{ inputs.build-args }}
|
|
||||||
|
|
||||||
merge:
|
|
||||||
needs: [matrix, build]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Download digests
|
|
||||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
|
||||||
with:
|
|
||||||
path: ${{ runner.temp }}/digests
|
|
||||||
pattern: ${{ needs.matrix.outputs.key }}-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
if: ${{ inputs.dockerhub-push }}
|
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
|
||||||
|
|
||||||
- name: Generate docker image tags
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
|
||||||
env:
|
|
||||||
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
|
||||||
with:
|
|
||||||
flavor: |
|
|
||||||
# Disable latest tag
|
|
||||||
latest=false
|
|
||||||
suffix=${{ inputs.tag-suffix }}
|
|
||||||
images: |
|
|
||||||
name=${{ env.GHCR_IMAGE }}
|
|
||||||
name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }}
|
|
||||||
tags: |
|
|
||||||
# Tag with branch name
|
|
||||||
type=ref,event=branch
|
|
||||||
# Tag with pr-number
|
|
||||||
type=ref,event=pr
|
|
||||||
# Tag with long commit sha hash
|
|
||||||
type=sha,format=long,prefix=commit-
|
|
||||||
# Tag with git tag on release
|
|
||||||
type=ref,event=tag
|
|
||||||
type=raw,value=release,enable=${{ github.event_name == 'release' }}
|
|
||||||
|
|
||||||
- name: Create manifest list and push
|
|
||||||
working-directory: ${{ runner.temp }}/digests
|
|
||||||
run: |
|
|
||||||
# Process annotations
|
|
||||||
declare -a ANNOTATIONS=()
|
|
||||||
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
|
|
||||||
while IFS= read -r annotation; do
|
|
||||||
# Extract key and value by removing the manifest: prefix
|
|
||||||
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
|
|
||||||
key="${BASH_REMATCH[1]}"
|
|
||||||
value="${BASH_REMATCH[2]}"
|
|
||||||
# Use array to properly handle arguments with spaces
|
|
||||||
ANNOTATIONS+=(--annotation "index:$key=$value")
|
|
||||||
fi
|
|
||||||
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
|
||||||
fi
|
|
||||||
|
|
||||||
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
|
||||||
SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *)
|
|
||||||
|
|
||||||
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
|
||||||
24
.github/workflows/pr-label-validation.yml
vendored
24
.github/workflows/pr-label-validation.yml
vendored
@@ -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@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
|
|
||||||
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.'
|
|
||||||
14
.github/workflows/pr-labeler.yml
vendored
14
.github/workflows/pr-labeler.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
name: 'Pull Request Labeler'
|
|
||||||
on:
|
|
||||||
- pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
labeler:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
|
|
||||||
@@ -4,16 +4,12 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-pr-title:
|
validate-pr-title:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: PR Conventional Commit Validation
|
- name: PR Conventional Commit Validation
|
||||||
uses: ytanikin/PRConventionalCommits@b628c5a234cc32513014b7bfdd1e47b532124d98 # 1.3.0
|
uses: ytanikin/PRConventionalCommits@1.1.0
|
||||||
with:
|
with:
|
||||||
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
|
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
|
||||||
add_label: 'false'
|
add_label: 'false'
|
||||||
|
|||||||
13
.github/workflows/pr-require-label.yml
vendored
Normal file
13
.github/workflows/pr-require-label.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Enforce PR labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, unlabeled, opened, edited, synchronize]
|
||||||
|
jobs:
|
||||||
|
enforce-label:
|
||||||
|
name: Enforce label
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- if: toJson(github.event.pull_request.labels) == '[]'
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
63
.github/workflows/prepare-release.yml
vendored
63
.github/workflows/prepare-release.yml
vendored
@@ -21,90 +21,63 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}-root
|
group: ${{ github.workflow }}-${{ github.ref }}-root
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
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@df432ceedc7162793a195dd1713ff69aefc7379e # v2
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ secrets.ORG_RELEASE_TOKEN }}
|
||||||
persist-credentials: true
|
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install Poetry
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
run: pipx install poetry
|
||||||
|
|
||||||
- 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
|
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@df432ceedc7162793a195dd1713ff69aefc7379e # v2
|
|
||||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
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
|
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@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
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: |
|
||||||
|
|||||||
35
.github/workflows/preview-label.yaml
vendored
35
.github/workflows/preview-label.yaml
vendored
@@ -1,35 +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
|
|
||||||
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' && contains(github.event.pull_request.labels.*.name, 'preview') }}
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
github.rest.issues.removeLabel({
|
|
||||||
issue_number: context.payload.pull_request.number,
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
name: 'preview'
|
|
||||||
})
|
|
||||||
14
.github/workflows/sdk.yml
vendored
14
.github/workflows/sdk.yml
vendored
@@ -4,26 +4,22 @@ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
99
.github/workflows/static_analysis.yml
vendored
99
.github/workflows/static_analysis.yml
vendored
@@ -9,81 +9,26 @@ 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.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- id: found_paths
|
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
mobile:
|
|
||||||
- 'mobile/**'
|
|
||||||
workflow:
|
|
||||||
- '.github/workflows/static_analysis.yml'
|
|
||||||
- name: Check if we should force jobs to run
|
|
||||||
id: should_force
|
|
||||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
mobile-dart-analyze:
|
mobile-dart-analyze:
|
||||||
name: Run Dart Code Analysis
|
name: Run Dart Code Analysis
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version: '3.22.0'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: dart pub get
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate translation file
|
|
||||||
run: make translation; dart format lib/generated/codegen_loader.g.dart
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Run Build Runner
|
|
||||||
run: make build
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Find file changes
|
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
|
|
||||||
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 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
|
working-directory: ./mobile
|
||||||
@@ -92,33 +37,7 @@ jobs:
|
|||||||
run: dart format lib/ --set-exit-if-changed
|
run: dart format lib/ --set-exit-if-changed
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run dart custom_lint
|
# Enable after riverpod generator migration is completed
|
||||||
run: dart run custom_lint
|
# - name: Run dart custom lint
|
||||||
working-directory: ./mobile
|
# run: dart run custom_lint
|
||||||
|
# working-directory: ./mobile
|
||||||
zizmor:
|
|
||||||
name: zizmor
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
|
||||||
run: uvx zizmor --format=sarif . > results.sarif
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload SARIF file
|
|
||||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
|
|
||||||
with:
|
|
||||||
sarif_file: results.sarif
|
|
||||||
category: zizmor
|
|
||||||
|
|||||||
484
.github/workflows/test.yml
vendored
484
.github/workflows/test.yml
vendored
@@ -9,78 +9,39 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-job:
|
doc-tests:
|
||||||
|
name: Docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
defaults:
|
||||||
contents: read
|
run:
|
||||||
outputs:
|
working-directory: ./docs
|
||||||
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_e2e: ${{ steps.found_paths.outputs.e2e == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_mobile: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- id: found_paths
|
- name: Run npm install
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
run: npm ci
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
web:
|
|
||||||
- 'web/**'
|
|
||||||
- 'i18n/**'
|
|
||||||
- 'open-api/typescript-sdk/**'
|
|
||||||
server:
|
|
||||||
- 'server/**'
|
|
||||||
cli:
|
|
||||||
- 'cli/**'
|
|
||||||
- 'open-api/typescript-sdk/**'
|
|
||||||
e2e:
|
|
||||||
- 'e2e/**'
|
|
||||||
mobile:
|
|
||||||
- 'mobile/**'
|
|
||||||
machine-learning:
|
|
||||||
- 'machine-learning/**'
|
|
||||||
workflow:
|
|
||||||
- '.github/workflows/test.yml'
|
|
||||||
.github:
|
|
||||||
- '.github/**'
|
|
||||||
|
|
||||||
- name: Check if we should force jobs to run
|
- name: Run formatter
|
||||||
id: should_force
|
run: npm run format
|
||||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: npm run build
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Test & Lint Server
|
name: Server
|
||||||
needs: pre-job
|
|
||||||
if: ${{ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './server/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -97,31 +58,25 @@ jobs:
|
|||||||
run: npm run check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run small tests & coverage
|
- name: Run unit tests & coverage
|
||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: Unit Test CLI
|
name: CLI
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version: 20
|
||||||
|
|
||||||
- name: Setup typescript-sdk
|
- name: Setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
@@ -146,65 +101,16 @@ jobs:
|
|||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests-win:
|
web-unit-tests:
|
||||||
name: Unit Test CLI (Windows)
|
name: Web
|
||||||
needs: pre-job
|
runs-on: ubuntu-latest
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
|
||||||
runs-on: windows-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./cli
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 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.
|
|
||||||
- name: Run tsc
|
|
||||||
run: npm run check
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run unit tests & coverage
|
|
||||||
run: npm run test:cov
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
web-lint:
|
|
||||||
name: Lint Web
|
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
|
|
||||||
runs-on: mich
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './web/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
@@ -214,7 +120,7 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint:p
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run formatter
|
- name: Run formatter
|
||||||
@@ -225,35 +131,6 @@ jobs:
|
|||||||
run: npm run check:svelte
|
run: npm run check:svelte
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
|
||||||
name: Test Web
|
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./web
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './web/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
|
||||||
run: npm ci && npm run build
|
|
||||||
working-directory: ./open-api/typescript-sdk
|
|
||||||
|
|
||||||
- name: Run npm install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run check:typescript
|
run: npm run check:typescript
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
@@ -262,103 +139,23 @@ jobs:
|
|||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-lint:
|
e2e-tests:
|
||||||
name: End-to-End Lint
|
name: End-to-End Tests
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './e2e/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
|
||||||
run: npm ci && npm run build
|
|
||||||
working-directory: ./open-api/typescript-sdk
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run linter
|
|
||||||
run: npm run lint
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run formatter
|
|
||||||
run: npm run format
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Run tsc
|
|
||||||
run: npm run check
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
server-medium-tests:
|
|
||||||
name: Medium Tests (Server)
|
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./server
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './server/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run npm install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run medium tests
|
|
||||||
run: npm run test:medium
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
e2e-tests-server-cli:
|
|
||||||
name: End-to-End Tests (Server & CLI)
|
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == '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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version: 20
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
@@ -374,47 +171,16 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Docker build
|
- name: Run linter
|
||||||
run: docker compose build
|
run: npm run lint
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run formatter
|
||||||
run: npm run test
|
run: npm run format
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
e2e-tests-web:
|
- name: Run tsc
|
||||||
name: End-to-End Tests (Web)
|
run: npm run check
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_e2e_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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
submodules: 'recursive'
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './e2e/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run setup typescript-sdk
|
|
||||||
run: npm ci && npm run build
|
|
||||||
working-directory: ./open-api/typescript-sdk
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
@@ -425,182 +191,98 @@ jobs:
|
|||||||
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:
|
|
||||||
- name: Any jobs failed?
|
|
||||||
if: ${{ contains(needs.*.result, 'failure') }}
|
|
||||||
run: exit 1
|
|
||||||
- name: All jobs passed or skipped
|
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
|
||||||
# zizmor: ignore[template-injection]
|
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Unit Test Mobile
|
name: Mobile
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version: '3.22.0'
|
||||||
- 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: ${{ needs.pre-job.outputs.should_run_ml == '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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- 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
|
||||||
|
cache: 'poetry'
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
|
||||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
||||||
# 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: ${{ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './.github/.nvmrc'
|
|
||||||
|
|
||||||
- name: Run npm install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run formatter
|
|
||||||
run: npm run 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run ShellCheck
|
- name: Run ShellCheck
|
||||||
uses: ludeeus/action-shellcheck@master
|
uses: ludeeus/action-shellcheck@master
|
||||||
with:
|
with:
|
||||||
ignore_paths: >-
|
ignore_paths: >-
|
||||||
**/open-api/**
|
**/open-api/**
|
||||||
**/openapi**
|
**/openapi/**
|
||||||
**/node_modules/**
|
**/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
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './server/.nvmrc'
|
|
||||||
|
|
||||||
- name: Install server dependencies
|
|
||||||
run: npm --prefix=server ci
|
|
||||||
|
|
||||||
- name: Build the app
|
|
||||||
run: npm --prefix=server run build
|
|
||||||
|
|
||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
|
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
|
|
||||||
|
|
||||||
- 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:
|
generated-typeorm-migrations-up-to-date:
|
||||||
name: SQL Schema Checks
|
name: TypeORM Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: tensorchord/vchord-postgres:pg14-v0.3.0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -618,14 +300,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: './server/.nvmrc'
|
|
||||||
|
|
||||||
- name: Install server dependencies
|
- name: Install server dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -634,38 +309,35 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: npm run 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: npm run 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: npm run 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
|
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: npm run sync:sql
|
run: npm run sql:generate
|
||||||
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
|
uses: tj-actions/verify-changed-files@v20
|
||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -673,11 +345,9 @@ jobs:
|
|||||||
|
|
||||||
- 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 }}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
# mobile-integration-tests:
|
# mobile-integration-tests:
|
||||||
|
|||||||
61
.github/workflows/weblate-lock.yml
vendored
61
.github/workflows/weblate-lock.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: Weblate checks
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-job:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
outputs:
|
|
||||||
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- id: found_paths
|
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
i18n:
|
|
||||||
- 'i18n/!(en)**\.json'
|
|
||||||
|
|
||||||
enforce-lock:
|
|
||||||
name: Check Weblate Lock
|
|
||||||
needs: [pre-job]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: {}
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
|
||||||
steps:
|
|
||||||
- name: Check weblate lock
|
|
||||||
run: |
|
|
||||||
if [[ "false" = $(curl https://hosted.weblate.org/api/components/immich/immich/lock/ | jq .locked) ]]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Find Pull Request
|
|
||||||
uses: juliangruber/find-pull-request-action@48b6133aa6c826f267ebd33aa2d29470f9d9e7d0 # v1
|
|
||||||
id: find-pr
|
|
||||||
with:
|
|
||||||
branch: chore/translations
|
|
||||||
- name: Fail if existing weblate PR
|
|
||||||
if: ${{ steps.find-pr.outputs.number }}
|
|
||||||
run: exit 1
|
|
||||||
success-check-lock:
|
|
||||||
name: Weblate Lock Check Success
|
|
||||||
needs: [enforce-lock]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: {}
|
|
||||||
if: always()
|
|
||||||
steps:
|
|
||||||
- name: Any jobs failed?
|
|
||||||
if: ${{ contains(needs.*.result, 'failure') }}
|
|
||||||
run: exit 1
|
|
||||||
- name: All jobs passed or skipped
|
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
|
||||||
# zizmor: ignore[template-injection]
|
|
||||||
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,5 +21,3 @@ mobile/openapi/.openapi-generator/FILES
|
|||||||
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-*
|
|
||||||
|
|||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -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
|
||||||
|
|||||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -5,8 +5,8 @@
|
|||||||
"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",
|
"remoteRoot": "/usr/src/app",
|
||||||
"localRoot": "${workspaceFolder}/server"
|
"localRoot": "${workspaceFolder}/server"
|
||||||
},
|
},
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
"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",
|
"remoteRoot": "/usr/src/app",
|
||||||
"localRoot": "${workspaceFolder}/server"
|
"localRoot": "${workspaceFolder}/server"
|
||||||
}
|
}
|
||||||
|
|||||||
75
.vscode/settings.json
vendored
75
.vscode/settings.json
vendored
@@ -1,63 +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"
|
||||||
},
|
}
|
||||||
"svelte.enable-ts-plugin": true,
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,4 +131,4 @@ conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|||||||
|
|
||||||
For answers to common questions about this code of conduct, see the
|
For answers to common questions about this code of conduct, see the
|
||||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||||
available at https://www.contributor-covenant.org/translations.
|
available at https://www.contributor-covenant.org/translations.
|
||||||
69
Makefile
69
Makefile
@@ -10,6 +10,12 @@ dev-update:
|
|||||||
dev-scale:
|
dev-scale:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
|
stage:
|
||||||
|
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
pull-stage:
|
||||||
|
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
@@ -17,9 +23,6 @@ e2e:
|
|||||||
prod:
|
prod:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-down:
|
|
||||||
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
@@ -34,65 +37,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:
|
||||||
npm --prefix server run sync:sql
|
npm --prefix server run sql:generate
|
||||||
|
|
||||||
attach-server:
|
attach-server:
|
||||||
docker exec -it docker_immich-server_1 sh
|
docker exec -it docker_immich-server_1 sh
|
||||||
|
|
||||||
renovate:
|
|
||||||
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
|
|
||||||
|
|
||||||
MODULES = e2e server web cli sdk docs .github
|
|
||||||
|
|
||||||
audit-%:
|
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
|
||||||
install-%:
|
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
|
|
||||||
build-cli: build-sdk
|
|
||||||
build-web: build-sdk
|
|
||||||
build-%: install-%
|
|
||||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
|
|
||||||
format-%:
|
|
||||||
npm --prefix $* run format:fix
|
|
||||||
lint-%:
|
|
||||||
npm --prefix $* run lint:fix
|
|
||||||
check-%:
|
|
||||||
npm --prefix $* run check
|
|
||||||
check-web:
|
|
||||||
npm --prefix web run check:typescript
|
|
||||||
npm --prefix web run check:svelte
|
|
||||||
test-%:
|
|
||||||
npm --prefix $* run test
|
|
||||||
test-e2e:
|
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
|
||||||
npm --prefix e2e run test
|
|
||||||
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 "npm ci && npm run test:medium -- --run"
|
|
||||||
test-medium-dev:
|
|
||||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
|
||||||
|
|
||||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
|
||||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
|
||||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
|
||||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
|
||||||
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
|
||||||
audit-all: $(foreach M,$(MODULES),audit-$M) ;
|
|
||||||
hygiene-all: lint-all format-all check-all sql audit-all;
|
|
||||||
test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
|
|
||||||
|
|
||||||
clean:
|
|
||||||
find . -name "node_modules" -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 "svelte-kit" -type d -prune -exec rm -rf '{}' +
|
|
||||||
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
|
||||||
docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
|
||||||
|
|||||||
81
README.md
81
README.md
@@ -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.gg/D8JsnBEuKb">
|
||||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
<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_ca_ES.md">Català</a>
|
||||||
<a href="readme_i18n/README_es_ES.md">Español</a>
|
<a href="readme_i18n/README_es_ES.md">Español</a>
|
||||||
<a href="readme_i18n/README_fr_FR.md">Français</a>
|
<a href="readme_i18n/README_fr_FR.md">Français</a>
|
||||||
@@ -29,13 +29,9 @@
|
|||||||
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
|
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
|
||||||
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
|
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
|
||||||
<a href="readme_i18n/README_zh_CN.md">中文</a>
|
<a href="readme_i18n/README_zh_CN.md">中文</a>
|
||||||
<a href="readme_i18n/README_uk_UA.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_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
|
||||||
@@ -45,34 +41,45 @@
|
|||||||
- ⚠️ **Do not use the app as the only way to store your photos and videos.**
|
- ⚠️ **Do not use the app as the only way to store your photos and videos.**
|
||||||
- ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
|
- ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
|
||||||
|
|
||||||
> [!NOTE]
|
## Content
|
||||||
> You can find the main documentation, including installation guides, at https://immich.app/.
|
|
||||||
|
|
||||||
## Links
|
- [Official Documentation](https://immich.app/docs)
|
||||||
|
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
||||||
- [Documentation](https://immich.app/docs)
|
|
||||||
- [About](https://immich.app/docs/overview/introduction)
|
|
||||||
- [Installation](https://immich.app/docs/install/requirements)
|
|
||||||
- [Roadmap](https://immich.app/roadmap)
|
|
||||||
- [Demo](#demo)
|
- [Demo](#demo)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Translations](https://immich.app/docs/developer/translations)
|
- [Introduction](https://immich.app/docs/overview/introduction)
|
||||||
- [Contributing](https://immich.app/docs/overview/support-the-project)
|
- [Installation](https://immich.app/docs/install/requirements)
|
||||||
|
- [Contribution Guidelines](https://immich.app/docs/overview/support-the-project)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
You can find the main documentation, including installation guides, at https://immich.app/.
|
||||||
|
|
||||||
## 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`.
|
You can access the web demo at https://demo.immich.app
|
||||||
|
|
||||||
### Login credentials
|
For the mobile app, you can use `https://demo.immich.app/api` for the `Server Endpoint URL`
|
||||||
|
|
||||||
| Email | Password |
|
```bash title="Demo Credential"
|
||||||
| --------------- | -------- |
|
The credential
|
||||||
| demo@immich.app | demo |
|
email: demo@immich.app
|
||||||
|
password: demo
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
|
```
|
||||||
|
|
||||||
|
## Activities
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
||||||
| Features | Mobile | Web |
|
| Features | Mobile | Web |
|
||||||
| :------------------------------------------- | ------ | --- |
|
| :--------------------------------------------- | -------- | ----- |
|
||||||
| Upload and view videos and photos | Yes | Yes |
|
| Upload and view videos and photos | Yes | Yes |
|
||||||
| Auto backup when the app is opened | Yes | N/A |
|
| Auto backup when the app is opened | Yes | N/A |
|
||||||
| Prevent duplication of assets | Yes | Yes |
|
| Prevent duplication of assets | Yes | Yes |
|
||||||
@@ -92,7 +99,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,22 +108,14 @@ 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
|
## Contributors
|
||||||
|
|
||||||
Read more about translations [here](https://immich.app/docs/developer/translations).
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||||
<a href="https://hosted.weblate.org/engage/immich/">
|
|
||||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Translation status" />
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Repository activity
|
## Star History
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Star history
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#immich-app/immich&Date">
|
<a href="https://star-history.com/#immich-app/immich&Date">
|
||||||
<picture>
|
<picture>
|
||||||
@@ -125,9 +124,3 @@ Read more about translations [here](https://immich.app/docs/developer/translatio
|
|||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
|
||||||
</a>
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please report security issues to `security@immich.app`
|
Please report security issues to `alex.tran1502@gmail.com`
|
||||||
1
cli/.eslintignore
Normal file
1
cli/.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/dist
|
||||||
28
cli/.eslintrc.cjs
Normal file
28
cli/.eslintrc.cjs
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1 +1 @@
|
|||||||
22.14.0
|
20.13
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core
|
FROM node:20-alpine3.19@sha256:291e84d956f1aff38454bbd3da38941461ad569a185c20aa289f71f37ea08e23 as core
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
@@ -16,4 +16,4 @@ RUN npm run build
|
|||||||
|
|
||||||
WORKDIR /import
|
WORKDIR /import
|
||||||
|
|
||||||
ENTRYPOINT ["node", "/usr/src/app/dist"]
|
ENTRYPOINT ["node", "/usr/src/app/dist"]
|
||||||
@@ -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 .
|
||||||
****
|
|
||||||
|
|||||||
@@ -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'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
6408
cli/package-lock.json
generated
6408
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.65",
|
"version": "2.2.0",
|
||||||
"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",
|
||||||
@@ -13,33 +13,29 @@
|
|||||||
"cli"
|
"cli"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
|
||||||
"@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.14.1",
|
"@types/node": "^20.3.1",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"@vitest/coverage-v8": "^1.2.2",
|
||||||
|
"byte-size": "^8.1.1",
|
||||||
"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.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^57.0.0",
|
"eslint-plugin-unicorn": "^53.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": "^3.2.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"vite": "^5.0.12",
|
||||||
"vite": "^6.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.4.0",
|
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -62,13 +58,10 @@
|
|||||||
"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",
|
"lodash-es": "^4.17.21"
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"micromatch": "^4.0.8"
|
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.14.0"
|
"node": "20.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
import * as fs from 'node:fs';
|
|
||||||
import * as os from 'node:os';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
import { setTimeout as sleep } from 'node:timers/promises';
|
|
||||||
import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
|
||||||
|
|
||||||
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
|
|
||||||
|
|
||||||
vi.mock('@immich/sdk');
|
|
||||||
|
|
||||||
describe('getAlbumName', () => {
|
|
||||||
it('should return a non-undefined value', () => {
|
|
||||||
if (os.platform() === 'win32') {
|
|
||||||
// This is meaningless for Unix systems.
|
|
||||||
expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test');
|
|
||||||
}
|
|
||||||
expect(getAlbumName('D:/parentfolder/test/Filename.txt', {} as UploadOptionsDto)).toBe('test');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has higher priority to return `albumName` in `options`', () => {
|
|
||||||
expect(getAlbumName('/parentfolder/test/Filename.txt', { albumName: 'example' } as UploadOptionsDto)).toBe(
|
|
||||||
'example',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('uploadFiles', () => {
|
|
||||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
||||||
const testFilePath = path.join(testDir, 'test.png');
|
|
||||||
const testFileData = 'test';
|
|
||||||
const baseUrl = 'http://example.com';
|
|
||||||
const apiKey = 'key';
|
|
||||||
const retry = 3;
|
|
||||||
|
|
||||||
const fetchMocker = createFetchMock(vi);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Create a test file
|
|
||||||
fs.writeFileSync(testFilePath, testFileData);
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
vi.mocked(defaults).baseUrl = baseUrl;
|
|
||||||
vi.mocked(defaults).headers = { 'x-api-key': apiKey };
|
|
||||||
|
|
||||||
fetchMocker.enableMocks();
|
|
||||||
fetchMocker.resetMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns new assets when upload file is successful', async () => {
|
|
||||||
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
|
|
||||||
{
|
|
||||||
filepath: testFilePath,
|
|
||||||
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns new assets when upload file retry is successful', async () => {
|
|
||||||
let counter = 0;
|
|
||||||
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
|
||||||
counter++;
|
|
||||||
if (counter < retry) {
|
|
||||||
throw new Error('Network error');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
|
|
||||||
{
|
|
||||||
filepath: testFilePath,
|
|
||||||
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns new assets when upload file retry is failed', async () => {
|
|
||||||
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
|
||||||
throw new Error('Network error');
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('checkForDuplicates', () => {
|
|
||||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
||||||
const testFilePath = path.join(testDir, 'test.png');
|
|
||||||
const testFileData = 'test';
|
|
||||||
const testFileChecksum = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'; // SHA1
|
|
||||||
const retry = 3;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Create a test file
|
|
||||||
fs.writeFileSync(testFilePath, testFileData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks duplicates', async () => {
|
|
||||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
action: Action.Accept,
|
|
||||||
id: testFilePath,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await checkForDuplicates([testFilePath], { concurrency: 1 });
|
|
||||||
|
|
||||||
expect(checkBulkUpload).toHaveBeenCalledWith({
|
|
||||||
assetBulkUploadCheckDto: {
|
|
||||||
assets: [
|
|
||||||
{
|
|
||||||
checksum: testFileChecksum,
|
|
||||||
id: testFilePath,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns duplicates when check duplicates is rejected', async () => {
|
|
||||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
action: Action.Reject,
|
|
||||||
id: testFilePath,
|
|
||||||
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
||||||
reason: Reason.Duplicate,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
||||||
duplicates: [
|
|
||||||
{
|
|
||||||
filepath: testFilePath,
|
|
||||||
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
newFiles: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns new assets when check duplicates is accepted', async () => {
|
|
||||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
action: Action.Accept,
|
|
||||||
id: testFilePath,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
||||||
duplicates: [],
|
|
||||||
newFiles: [testFilePath],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns results when check duplicates retry is successful', async () => {
|
|
||||||
let mocked = vi.mocked(checkBulkUpload);
|
|
||||||
for (let i = 1; i < retry; i++) {
|
|
||||||
mocked = mocked.mockRejectedValueOnce(new Error('Network error'));
|
|
||||||
}
|
|
||||||
mocked.mockResolvedValue({
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
action: Action.Accept,
|
|
||||||
id: testFilePath,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
||||||
duplicates: [],
|
|
||||||
newFiles: [testFilePath],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns results when check duplicates retry is failed', async () => {
|
|
||||||
vi.mocked(checkBulkUpload).mockRejectedValue(new Error('Network error'));
|
|
||||||
|
|
||||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
|
||||||
duplicates: [],
|
|
||||||
newFiles: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
AssetBulkUploadCheckItem,
|
|
||||||
AssetBulkUploadCheckResult,
|
AssetBulkUploadCheckResult,
|
||||||
AssetMediaResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
AssetMediaStatus,
|
|
||||||
addAssetsToAlbum,
|
addAssetsToAlbum,
|
||||||
checkBulkUpload,
|
checkBulkUpload,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
@@ -12,18 +10,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 os from 'node:os';
|
||||||
import path, { basename } from 'node:path';
|
import path, { basename } from 'node:path';
|
||||||
import { Queue } from 'src/queue';
|
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
|
||||||
import { BaseOptions, Batcher, 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');
|
||||||
|
|
||||||
@@ -31,7 +24,7 @@ const s = (count: number) => (count === 1 ? '' : 's');
|
|||||||
type AssetBulkUploadCheckResults = Array<AssetBulkUploadCheckResult & { id: string }>;
|
type AssetBulkUploadCheckResults = Array<AssetBulkUploadCheckResult & { id: string }>;
|
||||||
type Asset = { id: string; filepath: string };
|
type Asset = { id: string; filepath: string };
|
||||||
|
|
||||||
export interface UploadOptionsDto {
|
interface UploadOptionsDto {
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
ignore?: string;
|
ignore?: string;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
@@ -41,8 +34,6 @@ export interface UploadOptionsDto {
|
|||||||
albumName?: string;
|
albumName?: string;
|
||||||
includeHidden?: boolean;
|
includeHidden?: boolean;
|
||||||
concurrency: number;
|
concurrency: number;
|
||||||
progress?: boolean;
|
|
||||||
watch?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadFile extends File {
|
class UploadFile extends File {
|
||||||
@@ -62,94 +53,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);
|
|
||||||
await updateAlbums([...newAssets, ...duplicates], options);
|
|
||||||
await deleteFiles(newFiles, 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) => {
|
||||||
@@ -167,102 +83,48 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
|||||||
return files;
|
return files;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkForDuplicates = async (files: string[], { concurrency, skipHash, progress }: UploadOptionsDto) => {
|
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>(
|
try {
|
||||||
async (assets: AssetBulkUploadCheckItem[]) => {
|
// TODO refactor into a queue
|
||||||
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets } });
|
for (const items of chunk(files, concurrency)) {
|
||||||
|
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })));
|
||||||
|
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
|
||||||
|
|
||||||
const results = response.results as AssetBulkUploadCheckResults;
|
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) {
|
||||||
|
|
||||||
for (const { id: filepath, assetId, action } of results) {
|
|
||||||
if (action === Action.Accept) {
|
if (action === Action.Accept) {
|
||||||
newFiles.push(filepath);
|
newFiles.push(filepath);
|
||||||
} else {
|
} else {
|
||||||
// rejects are always duplicates
|
// rejects are always duplicates
|
||||||
duplicates.push({ id: assetId as string, filepath });
|
duplicates.push({ id: assetId as string, filepath });
|
||||||
}
|
}
|
||||||
|
progressBar.increment();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
checkProgressBar?.increment(assets.length);
|
} finally {
|
||||||
},
|
progressBar.stop();
|
||||||
{ 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;
|
|
||||||
},
|
|
||||||
{ concurrency, retry: 3 },
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const item of files) {
|
|
||||||
void queue.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await queue.drained();
|
|
||||||
|
|
||||||
if (checkBulkUploadRequests.length > 0) {
|
|
||||||
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)}`);
|
||||||
|
|
||||||
// Report failures
|
|
||||||
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
|
|
||||||
if (failedTasks.length > 0) {
|
|
||||||
console.log(`Failed to verify ${failedTasks.length} file${s(failedTasks.length)}:`);
|
|
||||||
for (const task of failedTasks) {
|
|
||||||
console.log(`- ${task.data} - ${task.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { newFiles, duplicates };
|
return { newFiles, duplicates };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadFiles = async (
|
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 [];
|
||||||
@@ -282,20 +144,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;
|
||||||
@@ -304,56 +158,41 @@ export const uploadFiles = async (
|
|||||||
|
|
||||||
const newAssets: Asset[] = [];
|
const newAssets: Asset[] = [];
|
||||||
|
|
||||||
const queue = new Queue<string, AssetMediaResponseDto>(
|
try {
|
||||||
async (filepath: string) => {
|
for (const items of chunk(files, concurrency)) {
|
||||||
const stats = statsMap.get(filepath);
|
await Promise.all(
|
||||||
if (!stats) {
|
items.map(async (filepath) => {
|
||||||
throw new Error(`Stats not found for ${filepath}`);
|
const stats = statsMap.get(filepath) as Stats;
|
||||||
}
|
const response = await uploadFile(filepath, stats);
|
||||||
|
|
||||||
const response = await uploadFile(filepath, stats);
|
newAssets.push({ id: response.id, filepath });
|
||||||
newAssets.push({ id: response.id, filepath });
|
|
||||||
if (response.status === AssetMediaStatus.Duplicate) {
|
|
||||||
duplicateCount++;
|
|
||||||
duplicateSize += stats.size ?? 0;
|
|
||||||
} else {
|
|
||||||
successCount++;
|
|
||||||
successSize += stats.size ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadProgress?.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
if (response.duplicate) {
|
||||||
|
duplicateCount++;
|
||||||
|
duplicateSize += stats.size ?? 0;
|
||||||
|
} else {
|
||||||
|
successCount++;
|
||||||
|
successSize += stats.size ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
||||||
},
|
|
||||||
{ concurrency, retry: 3 },
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const item of files) {
|
return response;
|
||||||
void queue.push(item);
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uploadProgress.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
await queue.drained();
|
|
||||||
|
|
||||||
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) {
|
||||||
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
|
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report failures
|
|
||||||
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
|
|
||||||
if (failedTasks.length > 0) {
|
|
||||||
console.log(`Failed to upload ${failedTasks.length} asset${s(failedTasks.length)}:`);
|
|
||||||
for (const task of failedTasks) {
|
|
||||||
console.log(`- ${task.data} - ${task.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newAssets;
|
return newAssets;
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
|
const uploadFile = async (input: string, stats: Stats): Promise<AssetFileUploadResponseDto> => {
|
||||||
const { baseUrl, headers } = defaults;
|
const { baseUrl, headers } = defaults;
|
||||||
|
|
||||||
const assetPath = path.parse(input);
|
const assetPath = path.parse(input);
|
||||||
@@ -386,7 +225,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
|||||||
formData.append('sidecarData', sidecarData);
|
formData.append('sidecarData', sidecarData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/assets`, {
|
const response = await fetch(`${baseUrl}/asset/upload`, {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
redirect: 'error',
|
redirect: 'error',
|
||||||
headers: headers as Record<string, string>,
|
headers: headers as Record<string, string>,
|
||||||
@@ -506,9 +345,7 @@ const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// `filepath` valid format:
|
const getAlbumName = (filepath: string, options: UploadOptionsDto) => {
|
||||||
// - Windows: `D:\\test\\Filename.txt` or `D:/test/Filename.txt`
|
const folderName = os.platform() === 'win32' ? filepath.split('\\').at(-2) : filepath.split('/').at(-2);
|
||||||
// - Unix: `/test/Filename.txt`
|
return options.albumName ?? folderName;
|
||||||
export const getAlbumName = (filepath: string, options: UploadOptionsDto) => {
|
|
||||||
return options.albumName ?? path.basename(path.dirname(filepath));
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getMyUser } from '@immich/sdk';
|
import { getMyUserInfo } from '@immich/sdk';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { mkdir, unlink } from 'node:fs/promises';
|
import { mkdir, unlink } from 'node:fs/promises';
|
||||||
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
|
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
|
||||||
@@ -10,13 +10,13 @@ export const login = async (url: string, key: string, options: BaseOptions) => {
|
|||||||
|
|
||||||
await connect(url, key);
|
await connect(url, key);
|
||||||
|
|
||||||
const [error, user] = await withError(getMyUser());
|
const [error, userInfo] = await withError(getMyUserInfo());
|
||||||
if (error) {
|
if (error) {
|
||||||
logError(error, 'Failed to load user info');
|
logError(error, 'Failed to load user info');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Logged in as ${user.email}`);
|
console.log(`Logged in as ${userInfo.email}`);
|
||||||
|
|
||||||
if (!existsSync(configDir)) {
|
if (!existsSync(configDir)) {
|
||||||
// Create config folder if it doesn't exist
|
// Create config folder if it doesn't exist
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
||||||
import { BaseOptions, authenticate } from 'src/utils';
|
import { BaseOptions, authenticate } from 'src/utils';
|
||||||
|
|
||||||
export const serverInfo = async (options: BaseOptions) => {
|
export const serverInfo = async (options: BaseOptions) => {
|
||||||
@@ -8,7 +8,7 @@ export const serverInfo = async (options: BaseOptions) => {
|
|||||||
getServerVersion(),
|
getServerVersion(),
|
||||||
getSupportedMediaTypes(),
|
getSupportedMediaTypes(),
|
||||||
getAssetStatistics({}),
|
getAssetStatistics({}),
|
||||||
getMyUser(),
|
getMyUserInfo(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(`Server Info (via ${userInfo.email})`);
|
console.log(`Server Info (via ${userInfo.email})`);
|
||||||
|
|||||||
@@ -69,13 +69,6 @@ program
|
|||||||
.default(4),
|
.default(4),
|
||||||
)
|
)
|
||||||
.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));
|
||||||
|
|
||||||
|
|||||||
131
cli/src/queue.ts
131
cli/src/queue.ts
@@ -1,131 +0,0 @@
|
|||||||
import * as fastq from 'fastq';
|
|
||||||
import { uniqueId } from 'lodash-es';
|
|
||||||
|
|
||||||
export type Task<T, R> = {
|
|
||||||
readonly id: string;
|
|
||||||
status: 'idle' | 'processing' | 'succeeded' | 'failed';
|
|
||||||
data: T;
|
|
||||||
error: unknown | undefined;
|
|
||||||
count: number;
|
|
||||||
// TODO: Could be useful to adding progress property.
|
|
||||||
// TODO: Could be useful to adding start_at/end_at/duration properties.
|
|
||||||
result: undefined | R;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueueOptions = {
|
|
||||||
verbose?: boolean;
|
|
||||||
concurrency?: number;
|
|
||||||
retry?: number;
|
|
||||||
// TODO: Could be useful to adding timeout property for retry.
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ComputedQueueOptions = Required<QueueOptions>;
|
|
||||||
|
|
||||||
export const defaultQueueOptions = {
|
|
||||||
concurrency: 1,
|
|
||||||
retry: 0,
|
|
||||||
verbose: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An in-memory queue that processes tasks in parallel with a given concurrency.
|
|
||||||
* @see {@link https://www.npmjs.com/package/fastq}
|
|
||||||
* @template T - The type of the worker task data.
|
|
||||||
* @template R - The type of the worker output data.
|
|
||||||
*/
|
|
||||||
export class Queue<T, R> {
|
|
||||||
private readonly queue: fastq.queueAsPromised<string, Task<T, R>>;
|
|
||||||
private readonly store = new Map<string, Task<T, R>>();
|
|
||||||
readonly options: ComputedQueueOptions;
|
|
||||||
readonly worker: (data: T) => Promise<R>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new queue.
|
|
||||||
* @param worker - The worker function that processes the task.
|
|
||||||
* @param options - The queue options.
|
|
||||||
*/
|
|
||||||
constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) {
|
|
||||||
this.options = { ...defaultQueueOptions, ...options };
|
|
||||||
this.worker = worker;
|
|
||||||
this.store = new Map<string, Task<T, R>>();
|
|
||||||
this.queue = this.buildQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
get tasks(): Task<T, R>[] {
|
|
||||||
const tasks: Task<T, R>[] = [];
|
|
||||||
for (const task of this.store.values()) {
|
|
||||||
tasks.push(task);
|
|
||||||
}
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTask(id: string): Task<T, R> {
|
|
||||||
const task = this.store.get(id);
|
|
||||||
if (!task) {
|
|
||||||
throw new Error(`Task with id ${id} not found`);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for the queue to be empty.
|
|
||||||
* @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`.
|
|
||||||
*/
|
|
||||||
drained(): Promise<void> {
|
|
||||||
return this.queue.drained();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a task at the end of the queue.
|
|
||||||
* @see {@link https://www.npmjs.com/package/fastq}
|
|
||||||
* @param data
|
|
||||||
* @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
|
|
||||||
* This promise could be ignored as it will not lead to a `unhandledRejection`.
|
|
||||||
*/
|
|
||||||
async push(data: T): Promise<Task<T, R>> {
|
|
||||||
const id = uniqueId();
|
|
||||||
const task: Task<T, R> = { id, status: 'idle', error: undefined, count: 0, data, result: undefined };
|
|
||||||
this.store.set(id, task);
|
|
||||||
return this.queue.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Support more function delegation to fastq.
|
|
||||||
|
|
||||||
private buildQueue(): fastq.queueAsPromised<string, Task<T, R>> {
|
|
||||||
return fastq.promise((id: string) => {
|
|
||||||
const task = this.getTask(id);
|
|
||||||
return this.work(task);
|
|
||||||
}, this.options.concurrency);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async work(task: Task<T, R>): Promise<Task<T, R>> {
|
|
||||||
task.count += 1;
|
|
||||||
task.error = undefined;
|
|
||||||
task.status = 'processing';
|
|
||||||
if (this.options.verbose) {
|
|
||||||
console.log('[task] processing:', task);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
task.result = await this.worker(task.data);
|
|
||||||
task.status = 'succeeded';
|
|
||||||
if (this.options.verbose) {
|
|
||||||
console.log('[task] succeeded:', task);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
} catch (error) {
|
|
||||||
task.error = error;
|
|
||||||
task.status = 'failed';
|
|
||||||
if (this.options.verbose) {
|
|
||||||
console.log('[task] failed:', task);
|
|
||||||
}
|
|
||||||
if (this.options.retry > 0 && task.count < this.options.retry) {
|
|
||||||
if (this.options.verbose) {
|
|
||||||
console.log('[task] retry:', task);
|
|
||||||
}
|
|
||||||
return this.work(task);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
import mockfs from 'mock-fs';
|
import mockfs from 'mock-fs';
|
||||||
import { readFileSync } from 'node:fs';
|
import { CrawlOptions, crawl } from 'src/utils';
|
||||||
import { Batcher, 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();
|
||||||
|
|
||||||
const readContent = (path: string) => {
|
|
||||||
return readFileSync(path).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
'.jpg',
|
'.jpg',
|
||||||
'.jpeg',
|
'.jpeg',
|
||||||
@@ -50,18 +43,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 +110,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: {
|
||||||
@@ -265,8 +256,7 @@ const tests: Test[] = [
|
|||||||
{
|
{
|
||||||
test: 'should support ignoring absolute paths',
|
test: 'should support ignoring absolute paths',
|
||||||
options: {
|
options: {
|
||||||
// Currently, fast-glob has some caveat when dealing with `/`.
|
pathsToCrawl: ['/'],
|
||||||
pathsToCrawl: ['/*s'],
|
|
||||||
recursive: true,
|
recursive: true,
|
||||||
exclusionPattern: '/images/**',
|
exclusionPattern: '/images/**',
|
||||||
},
|
},
|
||||||
@@ -284,58 +274,17 @@ 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);
|
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
it(name, async () => {
|
|
||||||
// The file contents is the same as the path.
|
|
||||||
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file])));
|
|
||||||
|
|
||||||
const actual = await crawl({ ...options, extensions });
|
const actual = await crawl({ ...options, extensions });
|
||||||
const expected = Object.entries(files)
|
const expected = Object.entries(files)
|
||||||
.filter((entry) => entry[1])
|
.filter((entry) => entry[1])
|
||||||
.map(([file]) => file);
|
.map(([file]) => file);
|
||||||
|
|
||||||
// Compare file's content instead of path since a file can be represent in multiple ways.
|
expect(actual.sort()).toEqual(expected.sort());
|
||||||
expect(actual.map((path) => readContent(path)).sort()).toEqual(expected.sort());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { getMyUser, init, isHttpError } from '@immich/sdk';
|
import { getMyUserInfo, init, isHttpError } from '@immich/sdk';
|
||||||
import { convertPathToPattern, glob } from 'fast-glob';
|
import { glob } from 'fast-glob';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
import { readFile, stat, writeFile } from 'node:fs/promises';
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
||||||
import { platform } from 'node:os';
|
|
||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ export const connect = async (url: string, key: string) => {
|
|||||||
|
|
||||||
init({ baseUrl: url, apiKey: key });
|
init({ baseUrl: url, apiKey: key });
|
||||||
|
|
||||||
const [error] = await withError(getMyUser());
|
const [error] = await withError(getMyUserInfo());
|
||||||
if (isHttpError(error)) {
|
if (isHttpError(error)) {
|
||||||
logError(error, 'Failed to connect to server');
|
logError(error, 'Failed to connect to server');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -107,11 +106,6 @@ export interface CrawlOptions {
|
|||||||
exclusionPattern?: string;
|
exclusionPattern?: string;
|
||||||
extensions: string[];
|
extensions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertPathToPatternOnWin = (path: string) => {
|
|
||||||
return platform() === 'win32' ? convertPathToPattern(path) : path;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||||
const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPattern, includeHidden } = options;
|
const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPattern, includeHidden } = options;
|
||||||
const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', ''));
|
const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', ''));
|
||||||
@@ -130,32 +124,36 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
|||||||
if (stats.isFile() || stats.isSymbolicLink()) {
|
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||||
crawledFiles.push(absolutePath);
|
crawledFiles.push(absolutePath);
|
||||||
} else {
|
} else {
|
||||||
patterns.push(convertPathToPatternOnWin(absolutePath));
|
patterns.push(absolutePath);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
patterns.push(convertPathToPatternOnWin(currentPath));
|
patterns.push(currentPath);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +170,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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
37
cli/src/version.ts
Normal file
37
cli/src/version.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { version } from '../package.json';
|
||||||
|
|
||||||
|
export interface ICliVersion {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CliVersion implements ICliVersion {
|
||||||
|
constructor(
|
||||||
|
public readonly major: number,
|
||||||
|
public readonly minor: number,
|
||||||
|
public readonly patch: number,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `${this.major}.${this.minor}.${this.patch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
const { major, minor, patch } = this;
|
||||||
|
return { major, minor, patch };
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromString(version: string): CliVersion {
|
||||||
|
const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
|
||||||
|
const matchResult = version.match(regex);
|
||||||
|
if (matchResult) {
|
||||||
|
const [, major, minor, patch] = matchResult.map(Number);
|
||||||
|
return new CliVersion(major, minor, patch);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid version format: ${version}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cliVersion = CliVersion.fromString(version);
|
||||||
@@ -2,7 +2,6 @@ import { defineConfig } from 'vite';
|
|||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: { alias: { src: '/src' } },
|
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: 'src/index.ts',
|
input: 'src/index.ts',
|
||||||
|
|||||||
38
deployment/.gitignore
vendored
38
deployment/.gitignore
vendored
@@ -1,38 +0,0 @@
|
|||||||
# OpenTofu
|
|
||||||
|
|
||||||
# Local .terraform directories
|
|
||||||
**/.terraform/*
|
|
||||||
|
|
||||||
# .tfstate files
|
|
||||||
*.tfstate
|
|
||||||
*.tfstate.*
|
|
||||||
|
|
||||||
# Crash log files
|
|
||||||
crash.log
|
|
||||||
crash.*.log
|
|
||||||
|
|
||||||
# Ignore override files as they are usually used to override resources locally and so
|
|
||||||
# are not checked in
|
|
||||||
override.tf
|
|
||||||
override.tf.json
|
|
||||||
*_override.tf
|
|
||||||
*_override.tf.json
|
|
||||||
|
|
||||||
# Include override files you do wish to add to version control using negated pattern
|
|
||||||
# !example_override.tf
|
|
||||||
|
|
||||||
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
|
|
||||||
# example: *tfplan*
|
|
||||||
|
|
||||||
# Ignore CLI configuration files
|
|
||||||
.terraformrc
|
|
||||||
terraform.rc
|
|
||||||
|
|
||||||
# Terragrunt
|
|
||||||
|
|
||||||
# terragrunt cache directories
|
|
||||||
**/.terragrunt-cache/*
|
|
||||||
|
|
||||||
# Terragrunt debug output file (when using `--terragrunt-debug` option)
|
|
||||||
# See: https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-debug
|
|
||||||
terragrunt-debug.tfvars.json
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# This file is maintained automatically by "tofu init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
|
||||||
version = "4.52.0"
|
|
||||||
constraints = "4.52.0"
|
|
||||||
hashes = [
|
|
||||||
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
|
||||||
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
|
||||||
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
|
||||||
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
|
||||||
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
|
||||||
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
|
||||||
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
|
||||||
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
|
||||||
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
|
||||||
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
|
||||||
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
|
||||||
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
|
||||||
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
|
||||||
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
|
||||||
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
|
||||||
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
|
||||||
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
|
||||||
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
|
||||||
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
|
||||||
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
|
||||||
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
|
||||||
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
|
||||||
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
|
||||||
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
|
||||||
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
|
||||||
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
|
||||||
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
|
||||||
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
terraform {
|
|
||||||
backend "pg" {}
|
|
||||||
required_version = "~> 1.7"
|
|
||||||
|
|
||||||
required_providers {
|
|
||||||
cloudflare = {
|
|
||||||
source = "cloudflare/cloudflare"
|
|
||||||
version = "4.52.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
resource "cloudflare_pages_domain" "immich_app_release_domain" {
|
|
||||||
account_id = var.cloudflare_account_id
|
|
||||||
project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name
|
|
||||||
domain = "immich.app"
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "cloudflare_record" "immich_app_release_domain" {
|
|
||||||
name = "immich.app"
|
|
||||||
proxied = true
|
|
||||||
ttl = 1
|
|
||||||
type = "CNAME"
|
|
||||||
content = 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
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
provider "cloudflare" {
|
|
||||||
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
data "terraform_remote_state" "api_keys_state" {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = var.tf_state_postgres_conn_str
|
|
||||||
schema_name = "prod_cloudflare_api_keys"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data "terraform_remote_state" "cloudflare_account" {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = var.tf_state_postgres_conn_str
|
|
||||||
schema_name = "prod_cloudflare_account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data "terraform_remote_state" "cloudflare_immich_app_docs" {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = var.tf_state_postgres_conn_str
|
|
||||||
schema_name = "prod_cloudflare_immich_app_docs_${var.prefix_name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
terraform {
|
|
||||||
source = "."
|
|
||||||
|
|
||||||
extra_arguments custom_vars {
|
|
||||||
commands = get_terraform_commands_that_need_vars()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
include {
|
|
||||||
path = find_in_parent_folders("state.hcl")
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_state {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
|
|
||||||
schema_name = "prod_cloudflare_immich_app_docs_release"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
variable "cloudflare_account_id" {}
|
|
||||||
variable "tf_state_postgres_conn_str" {}
|
|
||||||
|
|
||||||
variable "prefix_name" {}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# This file is maintained automatically by "tofu init".
|
|
||||||
# Manual edits may be lost in future updates.
|
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
|
||||||
version = "4.52.0"
|
|
||||||
constraints = "4.52.0"
|
|
||||||
hashes = [
|
|
||||||
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
|
||||||
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
|
||||||
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
|
||||||
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
|
||||||
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
|
||||||
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
|
||||||
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
|
||||||
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
|
||||||
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
|
||||||
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
|
||||||
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
|
||||||
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
|
||||||
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
|
||||||
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
|
||||||
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
|
||||||
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
|
||||||
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
|
||||||
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
|
||||||
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
|
||||||
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
|
||||||
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
|
||||||
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
|
||||||
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
|
||||||
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
|
||||||
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
|
||||||
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
|
||||||
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
|
||||||
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
terraform {
|
|
||||||
backend "pg" {}
|
|
||||||
required_version = "~> 1.7"
|
|
||||||
|
|
||||||
required_providers {
|
|
||||||
cloudflare = {
|
|
||||||
source = "cloudflare/cloudflare"
|
|
||||||
version = "4.52.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
resource "cloudflare_pages_domain" "immich_app_branch_domain" {
|
|
||||||
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
|
|
||||||
domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "cloudflare_record" "immich_app_branch_subdomain" {
|
|
||||||
name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
|
|
||||||
proxied = true
|
|
||||||
ttl = 1
|
|
||||||
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}"
|
|
||||||
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
|
|
||||||
}
|
|
||||||
|
|
||||||
output "immich_app_branch_subdomain" {
|
|
||||||
value = cloudflare_record.immich_app_branch_subdomain.hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
output "immich_app_branch_pages_hostname" {
|
|
||||||
value = cloudflare_record.immich_app_branch_subdomain.content
|
|
||||||
}
|
|
||||||
|
|
||||||
output "pages_project_name" {
|
|
||||||
value = cloudflare_pages_domain.immich_app_branch_domain.project_name
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
locals {
|
|
||||||
domain_name = "immich.app"
|
|
||||||
preview_prefix = contains(["branch", "pr"], var.prefix_event_type) ? "preview" : ""
|
|
||||||
archive_prefix = contains(["release"], var.prefix_event_type) ? "archive" : ""
|
|
||||||
deploy_domain_prefix = coalesce(local.preview_prefix, local.archive_prefix)
|
|
||||||
is_release = contains(["release"], var.prefix_event_type)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
provider "cloudflare" {
|
|
||||||
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
data "terraform_remote_state" "api_keys_state" {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = var.tf_state_postgres_conn_str
|
|
||||||
schema_name = "prod_cloudflare_api_keys"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data "terraform_remote_state" "cloudflare_account" {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = var.tf_state_postgres_conn_str
|
|
||||||
schema_name = "prod_cloudflare_account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
terraform {
|
|
||||||
source = "."
|
|
||||||
|
|
||||||
extra_arguments custom_vars {
|
|
||||||
commands = get_terraform_commands_that_need_vars()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
include {
|
|
||||||
path = find_in_parent_folders("state.hcl")
|
|
||||||
}
|
|
||||||
|
|
||||||
locals {
|
|
||||||
prefix_name = get_env("TF_VAR_prefix_name")
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_state {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
|
|
||||||
schema_name = "prod_cloudflare_immich_app_docs_${local.prefix_name}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
variable "cloudflare_account_id" {}
|
|
||||||
variable "tf_state_postgres_conn_str" {}
|
|
||||||
|
|
||||||
variable "prefix_name" {}
|
|
||||||
variable "prefix_event_type" {}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
locals {
|
|
||||||
cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID")
|
|
||||||
cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN")
|
|
||||||
|
|
||||||
tf_state_postgres_conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_state {
|
|
||||||
backend = "pg"
|
|
||||||
|
|
||||||
config = {
|
|
||||||
conn_str = local.tf_state_postgres_conn_str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
cloudflare_account_id = local.cloudflare_account_id
|
|
||||||
cloudflare_api_token = local.cloudflare_api_token
|
|
||||||
tf_state_postgres_conn_str = local.tf_state_postgres_conn_str
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -18,14 +9,11 @@ services:
|
|||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['/usr/src/app/bin/immich-dev']
|
command: ['/usr/src/app/bin/immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
|
||||||
# file: hwaccel.transcoding.yml
|
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app
|
||||||
- ../open-api:/usr/src/open-api
|
- ../open-api:/usr/src/open-api
|
||||||
@@ -35,52 +23,31 @@ services:
|
|||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
|
||||||
IMMICH_REPOSITORY: immich-app/immich
|
|
||||||
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
|
|
||||||
IMMICH_SOURCE_REF: local
|
|
||||||
IMMICH_SOURCE_COMMIT: af2efbdbbddc27cd06142f22253ccbbbbeec1f55
|
|
||||||
IMMICH_SOURCE_URL: https://github.com/immich-app/immich/commit/af2efbdbbddc27cd06142f22253ccbbbbeec1f55
|
|
||||||
IMMICH_BUILD: '9654404849'
|
|
||||||
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
|
|
||||||
IMMICH_BUILD_IMAGE: development
|
|
||||||
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/third-party
|
|
||||||
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
|
||||||
- database
|
- database
|
||||||
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
|
||||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
|
||||||
# user: 0:0
|
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
command: ['/usr/src/app/bin/immich-web']
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 2283:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app
|
||||||
- ../i18n:/usr/src/i18n
|
|
||||||
- ../open-api/:/usr/src/open-api/
|
- ../open-api/:/usr/src/open-api/
|
||||||
# - ../../ui:/usr/ui
|
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
@@ -95,12 +62,12 @@ services:
|
|||||||
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:
|
||||||
@@ -111,18 +78,14 @@ 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:4a9f847af90037d59b34cd4d4ad14c6e055f46540cf4ff757aaafb266060fa28
|
image: redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
|
||||||
healthcheck:
|
|
||||||
test: redis-cli ping || exit 1
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -134,8 +97,9 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
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_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:
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
#
|
|
||||||
# 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:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
# extends:
|
|
||||||
# file: hwaccel.transcoding.yml
|
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
@@ -24,46 +12,38 @@ services:
|
|||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 2283:2283
|
- 2283:3001
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
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:
|
|
||||||
- 3003:3003
|
|
||||||
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:4a9f847af90037d59b34cd4d4ad14c6e055f46540cf4ff757aaafb266060fa28
|
image: redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
|
||||||
healthcheck:
|
|
||||||
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.3.0-pgvectors0.2.0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -75,14 +55,14 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
restart: always
|
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_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:e2b8aa62b64855956e3ec1e18b4f9387fb6203174a4471936f4662f437f04405
|
image: prom/prometheus@sha256:5c435642ca4d8427ca26f4901c11114023004709037880cd7860d5b7176aa731
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@@ -91,10 +71,10 @@ services:
|
|||||||
# add data source for http://immich-prometheus:9090 to get started
|
# add data source for http://immich-prometheus:9090 to get started
|
||||||
immich-grafana:
|
immich-grafana:
|
||||||
container_name: immich_grafana
|
container_name: immich_grafana
|
||||||
command: [ './run.sh', '-disable-reporting' ]
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50
|
image: grafana/grafana:11.0.0-ubuntu@sha256:02e99d1ee0b52dc9d3000c7b5314e7a07e0dfd69cc49bb3f8ce323491ed3406b
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -13,61 +12,49 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
# extends:
|
|
||||||
# file: hwaccel.transcoding.yml
|
|
||||||
# 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}:/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
|
||||||
# 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:4a9f847af90037d59b34cd4d4ad14c6e055f46540cf4ff757aaafb266060fa28
|
image: docker.io/redis:6.2-alpine@sha256:c0634a08e74a4bb576d02d1ee993dc05dba10e8b7b9492dfa28a7af100d46c01
|
||||||
healthcheck:
|
|
||||||
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.3.0-pgvectors0.2.0
|
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'
|
||||||
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
|
||||||
# change ssd below to hdd if you are using a hard disk drive or other slow storage
|
|
||||||
command: postgres -c config_file=/etc/postgresql/postgresql.ssd.conf
|
|
||||||
restart: always
|
restart: always
|
||||||
|
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"]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
|||||||
@@ -2,18 +2,13 @@
|
|||||||
|
|
||||||
# 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
|
|
||||||
# TZ=Etc/UTC
|
|
||||||
|
|
||||||
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
|
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
|
||||||
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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ global:
|
|||||||
evaluation_interval: 15s
|
evaluation_interval: 15s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: immich_api
|
- job_name: immich_server
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['immich-server:8081']
|
- targets: ['immich-server:8081']
|
||||||
|
|
||||||
- job_name: immich_microservices
|
- job_name: immich_microservices
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['immich-server:8082']
|
- targets: ['immich-microservices:8081']
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
LOG_LEVEL="${IMMICH_LOG_LEVEL:='info'}"
|
|
||||||
|
|
||||||
logDebug() {
|
|
||||||
if [ "$LOG_LEVEL" = "debug" ] || [ "$LOG_LEVEL" = "verbose" ]; then
|
|
||||||
echo "DEBUG: $1" >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
|
|
||||||
logDebug "cgroup v2 detected."
|
|
||||||
if [ -f /sys/fs/cgroup/cpu.max ]; then
|
|
||||||
read -r quota period </sys/fs/cgroup/cpu.max
|
|
||||||
if [ "$quota" = "max" ]; then
|
|
||||||
logDebug "No CPU limits set."
|
|
||||||
unset quota period
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
logDebug "/sys/fs/cgroup/cpu.max not found."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
logDebug "cgroup v1 detected."
|
|
||||||
|
|
||||||
if [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ] && [ -f /sys/fs/cgroup/cpu/cpu.cfs_period_us ]; then
|
|
||||||
quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)
|
|
||||||
period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)
|
|
||||||
|
|
||||||
if [ "$quota" = "-1" ]; then
|
|
||||||
logDebug "No CPU limits set."
|
|
||||||
unset quota period
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
logDebug "/sys/fs/cgroup/cpu/cpu.cfs_quota_us or /sys/fs/cgroup/cpu/cpu.cfs_period_us not found."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${quota:-}" ] && [ -n "${period:-}" ]; then
|
|
||||||
cpus=$((quota / period))
|
|
||||||
if [ "$cpus" -eq 0 ]; then
|
|
||||||
cpus=1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
cpus=$(grep -c ^processor /proc/cpuinfo)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$cpus"
|
|
||||||
@@ -1 +1 @@
|
|||||||
22.14.0
|
20.13
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full
|
|||||||
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
|
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
|
||||||
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
|
- 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
|
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
|
||||||
|
|
||||||
Cheer!
|
Cheer!
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full
|
|||||||
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
|
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
|
||||||
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
|
- 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
|
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
|
||||||
|
|
||||||
Cheer!
|
Cheer!
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: The Immich core team goes full-time
|
title: The Immich core team goes full-time
|
||||||
authors: [alextran]
|
authors: [alextran]
|
||||||
tags: [update, announcement, FUTO]
|
tags: [update, announcement, futo]
|
||||||
date: 2024-05-01T00:00
|
date: 2024-05-01T00:00
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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!
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user