Compare commits

..

3 Commits

Author SHA1 Message Date
Min Idzelis 6fffe41f0b Remaining 2025-07-02 01:58:36 +00:00
Daimolean 83afd49f5c feat(mobile): edit location action (#19645)
* change dto from integer to double

* feat(mobile): edit location action

* patch openapi

* refactor in provider

* fix lint

* chore: not showing success prompt if dimissed

* i18n

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-07-01 16:52:11 +00:00
Ramon Smits 639ede78c2 docs: document DB_STORAGE_TYPE environment variable (#19609)
Co-authored-by: Zack Pollard <github@zackpollard.uk>
2025-07-01 16:13:24 +00:00
61 changed files with 27031 additions and 62640 deletions
+2 -4
View File
@@ -73,10 +73,8 @@ install_dependencies() {
log "Installing dependencies"
(
cd "${IMMICH_WORKSPACE}" || exit 1
run_cmd make ci-server
run_cmd make ci-sdk
run_cmd make build-sdk
run_cmd make ci-web
export CI=1 FROZEN=1 OFFLINE=1
run_cmd make setup-dev
)
log ""
}
@@ -22,7 +22,7 @@ services:
immich-machine-learning:
env_file: !reset []
database:
env_file: !reset []
environment: !override
@@ -31,7 +31,7 @@ services:
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
POSTGRES_INITDB_ARGS: '--data-checksums'
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
volumes:
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
redis:
@@ -10,8 +10,9 @@ cd "${IMMICH_WORKSPACE}/server" || (
exit 1
)
CI=1 pnpm install
while true; do
run_cmd node ./node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch
run_cmd pnpm exec nest start --debug "0.0.0.0:9230" --watch
log "Nest API Server crashed with exit code $?. Respawning in 3s ..."
sleep 3
done
@@ -16,7 +16,7 @@ until curl --output /dev/null --silent --head --fail "http://127.0.0.1:${IMMICH_
done
while true; do
run_cmd node ./node_modules/.bin/vite dev --host 0.0.0.0 --port "${DEV_PORT}"
run_cmd pnpm exec vite dev --host 0.0.0.0 --port "${DEV_PORT}"
log "Web crashed with exit code $?. Respawning in 3s ..."
sleep 3
done
+4
View File
@@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
-28
View File
@@ -1,28 +0,0 @@
{
"name": ".github",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"prettier": "^3.5.3"
}
},
"node_modules/prettier": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz",
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
}
}
}
-82
View File
@@ -1,82 +0,0 @@
name: Check Team Approval
on:
workflow_call:
jobs:
check-approval:
runs-on: ubuntu-latest
permissions:
pull-requests: read
contents: read
steps:
- name: Check for team/admin review
id: check-review
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
console.log(`Checking reviews for PR #${prNumber}`);
try {
// Fetch the users.json file from immich-app/devtools repository
const { data: usersFile } = await github.rest.repos.getContent({
owner: 'immich-app',
repo: 'devtools',
path: 'tf/deployment/data/users.json'
});
const usersData = JSON.parse(Buffer.from(usersFile.content, 'base64').toString());
console.log(`Loaded ${usersData.length} users from devtools repo`);
// Create a map of GitHub IDs to user roles for efficient lookup
const userRoles = new Map();
for (const user of usersData) {
if (user.github && user.github.id && (user.role === 'team' || user.role === 'admin')) {
userRoles.set(user.github.id, {
username: user.github.username,
role: user.role
});
}
}
console.log(`Found ${userRoles.size} team/admin users`);
// Get all reviews for the pull request
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber
});
console.log(`Found ${reviews.length} reviews`);
// Check if any review is from a team/admin member
let hasValidReview = false;
for (const review of reviews) {
console.log(`Review by ${review.user.login} (ID: ${review.user.id}): state=${review.state}`);
// Check if the reviewer is a team/admin member and the review is approved
const userInfo = userRoles.get(review.user.id);
if (userInfo && review.state === 'APPROVED') {
console.log(`✅ Found approved review from ${userInfo.role} member: ${review.user.login}`);
hasValidReview = true;
break;
}
}
if (!hasValidReview) {
console.log('❌ No approved review from team/admin member found');
core.setFailed('This pull request requires an approved review from a team or admin member');
} else {
console.log('✅ Required team/admin member review found');
}
} catch (error) {
console.error('Error checking reviews:', error);
core.setFailed(`Failed to check reviews: ${error.message}`);
}
+17 -14
View File
@@ -33,21 +33,24 @@ jobs:
with:
persist-credentials: false
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/
- name: Build SDK
run: npm run build --prefix ../open-api/typescript-sdk/
- run: npm ci
- run: npm run build
- run: npm publish
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish
if: ${{ github.event_name == 'release' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+10 -7
View File
@@ -53,21 +53,24 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './docs/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run npm install
run: npm ci
- name: Run install
run: pnpm install
- name: Check formatting
run: npm run format
run: pnpm format
- name: Run build
run: npm run build
run: pnpm build
- name: Upload build output
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Fix formatting
run: make install-all && make format-all
-11
View File
@@ -1,11 +0,0 @@
name: Required Reviewers Check
on:
pull_request_review:
jobs:
check-member-review:
uses: ./.github/workflows/check-team-approval.yml
permissions:
pull-requests: read
contents: read
+8 -5
View File
@@ -20,18 +20,21 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install deps
run: npm ci
run: pnpm install --frozen-lockfile
- name: Build
run: npm run build
run: pnpm build
- name: Publish
run: npm publish
run: pnpm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+129 -85
View File
@@ -80,30 +80,33 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run npm install
run: npm ci
- name: Run package manager install
run: pnpm install
- name: Run linter
run: npm run lint
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
run: pnpm check
if: ${{ !cancelled() }}
- name: Run small tests & coverage
run: npm test
run: pnpm test
if: ${{ !cancelled() }}
cli-unit-tests:
@@ -123,34 +126,37 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: npm ci && npm run build
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: npm ci
run: pnpm install
- name: Run linter
run: npm run lint
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test
run: pnpm test
if: ${{ !cancelled() }}
cli-unit-tests-win:
@@ -170,27 +176,30 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './cli/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: npm ci
run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test.
- name: Run tsc
run: npm run check
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test
run: pnpm test
if: ${{ !cancelled() }}
web-lint:
@@ -210,30 +219,33 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: npm ci
run: pnpm rebuild && pnpm install --frozen-lockfile
- name: Run linter
run: npm run lint:p
run: pnpm lint:p
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
run: pnpm format
if: ${{ !cancelled() }}
- name: Run svelte checks
run: npm run check:svelte
run: pnpm check:svelte
if: ${{ !cancelled() }}
web-unit-tests:
@@ -253,26 +265,29 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: npm ci
run: pnpm install --frozen-lockfile
- name: Run tsc
run: npm run check:typescript
run: pnpm check:typescript
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test
run: pnpm test
if: ${{ !cancelled() }}
i18n-tests:
@@ -288,18 +303,21 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './web/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install dependencies
run: npm --prefix=web ci
run: pnpm --filter=immich-web install --frozen-lockfile
- name: Format
run: npm --prefix=web run format:i18n
run: pnpm --filter=immich-web format:i18n
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -334,32 +352,35 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Run linter
run: npm run lint
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
run: pnpm check
if: ${{ !cancelled() }}
server-medium-tests:
@@ -379,18 +400,21 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run npm install
run: npm ci
run: pnpm install --frozen-lockfile
- name: Run medium tests
run: npm run test:medium
run: pnpm test:medium
if: ${{ !cancelled() }}
e2e-tests-server-cli:
@@ -414,25 +438,33 @@ jobs:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- name: Run setup cli
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Docker build
@@ -440,7 +472,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
run: npm run test
run: pnpm test
if: ${{ !cancelled() }}
e2e-tests-web:
@@ -464,20 +496,23 @@ jobs:
persist-credentials: false
submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: npm ci && npm run build
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
@@ -584,18 +619,21 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './.github/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run npm install
run: npm ci
run: pnpm install --frozen-lockfile
- name: Run formatter
run: npm run format
run: pnpm format
if: ${{ !cancelled() }}
shellcheck:
@@ -627,18 +665,21 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install server dependencies
run: npm --prefix=server ci
run: pnpm --filter immich install --frozen-lockfile
- name: Build the app
run: npm --prefix=server run build
run: pnpm --filter immich build
- name: Run API generation
run: make open-api
@@ -690,28 +731,31 @@ jobs:
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: './server/.nvmrc'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install server dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Build the app
run: npm run build
run: pnpm build
- name: Run existing migrations
run: npm run migrations:run
run: pnpm migrations:run
- name: Test npm run schema:reset command works
run: npm run schema:reset
run: pnpm schema:reset
- name: Generate new migrations
continue-on-error: true
run: npm run migrations:generate src/TestMigration
run: pnpm migrations:generate src/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -730,7 +774,7 @@ jobs:
exit 1
- name: Run SQL generation
run: npm run sync:sql
run: pnpm sync:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
+44 -27
View File
@@ -40,7 +40,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql:
npm --prefix server run sync:sql
pnpm --filter immich run sync:sql
attach-server:
docker exec -it docker_immich-server_1 sh
@@ -50,31 +50,40 @@ renovate:
MODULES = e2e server web cli sdk docs .github
# directory to package name mapping function
# cli = @immich/cli
# docs = documentation
# e2e = immich-e2e
# open-api/typescript-sdk = @immich/sdk
# server = immich
# web = immich-web
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
audit-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
pnpm --filter $(call map-package,$*) audit fix
install-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
ci-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
build-cli: build-sdk
build-web: build-sdk
build-%: install-%
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build
pnpm --filter $(call map-package,$*) run build
format-%:
npm --prefix $* run format:fix
pnpm --filter $(call map-package,$*) run format:fix
lint-%:
npm --prefix $* run lint:fix
pnpm --filter $(call map-package,$*) run lint:fix
lint-web:
pnpm --filter $(call map-package,$*) run lint:p
check-%:
npm --prefix $* run check
pnpm --filter $(call map-package,$*) run check
check-web:
npm --prefix web run check:typescript
npm --prefix web run check:svelte
pnpm --filter immich-web run check:typescript
pnpm --filter immich-web run check:svelte
test-%:
npm --prefix $* run test
pnpm --filter $(call map-package,$*) run test
test-e2e:
docker compose -f ./e2e/docker-compose.yml build
npm --prefix e2e run test
npm --prefix e2e run test:web
pnpm --filter immich-e2e run test
pnpm --filter immich-e2e run test:web
test-medium:
docker run \
--rm \
@@ -84,19 +93,28 @@ test-medium:
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
-e NODE_ENV=development \
immich-server:latest \
-c "npm ci && npm run test:medium -- --run"
-c "pnpm test:medium -- --run"
test-medium-dev:
docker exec -it immich_server /bin/sh -c "npm run test:medium"
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
install-all: $(foreach M,$(MODULES),install-$M) ;
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$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) ;
install-all:
pnpm -r --filter '!documentation' install
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
check-all:
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
lint-all:
pnpm -r --filter '!documentation' run lint:fix
format-all:
pnpm -r --filter '!documentation' run format:fix
audit-all:
pnpm -r --filter '!documentation' audit fix
hygiene-all: audit-all
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
test-all:
pnpm -r --filter '!documentation' run "/^test/"
clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} +
@@ -105,5 +123,4 @@ clean:
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
setup-dev: install-server install-sdk build-sdk install-web
setup-dev: install-server install-sdk build-sdk install-web
+32 -10
View File
@@ -1,19 +1,41 @@
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
RUN npm ci
COPY open-api/typescript-sdk/ ./
RUN npm run build
ENV COREPACK_ENABLE_AUTO_PIN=0 \
COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable && \
corepack install -g pnpm
WORKDIR /usr/src/app
COPY --chown=node:node . .
COPY cli/package.json cli/package-lock.json ./
RUN npm ci
WORKDIR /usr/src/app/web
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm exec svelte-kit sync
COPY cli .
RUN npm run build
WORKDIR /usr/src/app/open-api/typescript-sdk
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm build
WORKDIR /usr/src/app/cli
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile --prod --no-optional && \
pnpm build
RUN rm -rf /usr/src/app/web && \
rm -rf /usr/src/app/open-api && \
rm -rf /usr/src/app/cli/src && \
rm -rf /usr/src/app/cli/src && \
rm -rf /usr/src/app/server && \
rm -rf /usr/src/app/i18n && \
rm -rf /usr/src/app/e2e && \
rm -rf /usr/src/app/docs && \
rm -rf /usr/src/app/readme_i18n && \
rm -rf /usr/src/app/deployment && \
rm -rf /usr/src/app/docker
WORKDIR /import
ENTRYPOINT ["node", "/usr/src/app/dist"]
ENTRYPOINT ["node", "/usr/src/app/cli/dist"]
+8 -4
View File
@@ -6,8 +6,10 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
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
# if you don't have node installed
$ npm install -g pnpm
$ pnpm install
$ pnpm build
Then, to build the open-api client run the following in the open-api folder:
@@ -15,8 +17,10 @@ Then, to build the open-api client run the following in the open-api folder:
To run the Immich CLI from source, run the following in the cli folder:
$ npm install
$ npm run build
# if you don't have node installed
$ npm install -g pnpm
$ pnpm install
$ pnpm build
$ ts-node .
You'll need ts-node, the easiest way to install it is to use npm:
-4617
View File
File diff suppressed because it is too large Load Diff
+9 -11
View File
@@ -16,7 +16,7 @@ name: immich-dev
services:
immich-server:
container_name: immich_server
command: ['/usr/src/app/bin/immich-dev']
command: ['/usr/src/app/server/bin/immich-dev']
image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
@@ -24,13 +24,12 @@ services:
build:
context: ../
dockerfile: server/Dockerfile
target: dev
target: dev-docker
restart: unless-stopped
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- ..:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/usr/src/app/server/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/server/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
@@ -69,17 +68,16 @@ services:
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
# user: 0:0
build:
context: ../web
command: ['/usr/src/app/bin/immich-web']
context: ../
dockerfile: web/Dockerfile
command: ['/usr/src/app/web/bin/immich-web']
env_file:
- .env
ports:
- 3000:3000
- 24678:24678
volumes:
- ../web:/usr/src/app
- ../i18n:/usr/src/i18n
- ../open-api/:/usr/src/open-api/
- ..:/usr/src/app
# - ../../ui:/usr/ui
- /usr/src/app/node_modules
ulimits:
+5
View File
@@ -1,2 +1,7 @@
build/
.docusaurus/
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
+1 -1
View File
@@ -5,7 +5,7 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
### Installation
```
$ npm install
$ pnpm install
```
### Local Development
+1 -1
View File
@@ -200,7 +200,7 @@ When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user
- Installs dependencies: `npm install` in all packages
- Installs dependencies: `pnpm install` in all packages
- Builds TypeScript SDK: `npm run build` in `open-api/typescript-sdk`
2. **Starts development servers** via VS Code tasks:
+1 -1
View File
@@ -56,7 +56,7 @@ If you only want to do web development connected to an existing, remote backend,
1. Build the Immich SDK - `cd open-api/typescript-sdk && npm i && npm run build && cd -`
2. Enter the web directory - `cd web/`
3. Install web dependencies - `npm i`
3. Install web dependencies - `pnpm i`
4. Start the web development server
```bash
+1 -1
View File
@@ -5,7 +5,7 @@
### Unit tests
Unit are run by calling `npm run test` from the `server/` directory.
You need to run `npm install` (in `server/`) before _once_.
You need to run `pnpm install` (in `server/`) before _once_.
### End to end tests
+14 -11
View File
@@ -72,22 +72,25 @@ Information on the current workers can be found [here](/docs/administration/jobs
## Database
| Variable | Description | Default | Containers |
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
| `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database host | `database` | server |
| `DB_PORT` | Database port | `5432` | server |
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| Variable | Description | Default | Containers |
| :---------------------------------- | :------------------------------------------------------------------------------------- | :--------: | :----------------------------- |
| `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database host | `database` | server |
| `DB_PORT` | Database port | `5432` | server |
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | server |
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs.
:::info
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
-20545
View File
File diff suppressed because it is too large Load Diff
-7469
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
import request from 'supertest';
export type { Emitter } from '@socket.io/component-emitter';
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
@@ -78,16 +79,16 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) =>
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const executeCommand = (command: string, args: string[]) => {
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
let _resolve: (value: CommandResponse) => void;
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
let stdout = '';
let stderr = '';
+1
View File
@@ -799,6 +799,7 @@
"edit_key": "Edit key",
"edit_link": "Edit link",
"edit_location": "Edit location",
"edit_location_action_prompt": "{count} location edited",
"edit_location_dialog_title": "Location",
"edit_name": "Edit name",
"edit_people": "Edit people",
File diff suppressed because one or more lines are too long
@@ -116,15 +116,15 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
TextColumn get exposureTime => text().nullable()();
IntColumn get fNumber => integer().nullable()();
RealColumn get fNumber => real().nullable()();
IntColumn get fileSize => integer().nullable()();
IntColumn get focalLength => integer().nullable()();
RealColumn get focalLength => real().nullable()();
IntColumn get latitude => integer().nullable()();
RealColumn get latitude => real().nullable()();
IntColumn get longitude => integer().nullable()();
RealColumn get longitude => real().nullable()();
IntColumn get iso => integer().nullable()();
+77 -77
View File
@@ -19,11 +19,11 @@ typedef $$RemoteExifEntityTableCreateCompanionBuilder
i0.Value<int?> height,
i0.Value<int?> width,
i0.Value<String?> exposureTime,
i0.Value<int?> fNumber,
i0.Value<double?> fNumber,
i0.Value<int?> fileSize,
i0.Value<int?> focalLength,
i0.Value<int?> latitude,
i0.Value<int?> longitude,
i0.Value<double?> focalLength,
i0.Value<double?> latitude,
i0.Value<double?> longitude,
i0.Value<int?> iso,
i0.Value<String?> make,
i0.Value<String?> model,
@@ -43,11 +43,11 @@ typedef $$RemoteExifEntityTableUpdateCompanionBuilder
i0.Value<int?> height,
i0.Value<int?> width,
i0.Value<String?> exposureTime,
i0.Value<int?> fNumber,
i0.Value<double?> fNumber,
i0.Value<int?> fileSize,
i0.Value<int?> focalLength,
i0.Value<int?> latitude,
i0.Value<int?> longitude,
i0.Value<double?> focalLength,
i0.Value<double?> latitude,
i0.Value<double?> longitude,
i0.Value<int?> iso,
i0.Value<String?> make,
i0.Value<String?> model,
@@ -125,20 +125,20 @@ class $$RemoteExifEntityTableFilterComposer
column: $table.exposureTime,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get fNumber => $composableBuilder(
i0.ColumnFilters<double> get fNumber => $composableBuilder(
column: $table.fNumber, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get fileSize => $composableBuilder(
column: $table.fileSize, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get focalLength => $composableBuilder(
i0.ColumnFilters<double> get focalLength => $composableBuilder(
column: $table.focalLength,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get latitude => $composableBuilder(
i0.ColumnFilters<double> get latitude => $composableBuilder(
column: $table.latitude, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get longitude => $composableBuilder(
i0.ColumnFilters<double> get longitude => $composableBuilder(
column: $table.longitude, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get iso => $composableBuilder(
@@ -223,20 +223,20 @@ class $$RemoteExifEntityTableOrderingComposer
column: $table.exposureTime,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get fNumber => $composableBuilder(
i0.ColumnOrderings<double> get fNumber => $composableBuilder(
column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get fileSize => $composableBuilder(
column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get focalLength => $composableBuilder(
i0.ColumnOrderings<double> get focalLength => $composableBuilder(
column: $table.focalLength,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get latitude => $composableBuilder(
i0.ColumnOrderings<double> get latitude => $composableBuilder(
column: $table.latitude, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get longitude => $composableBuilder(
i0.ColumnOrderings<double> get longitude => $composableBuilder(
column: $table.longitude,
builder: (column) => i0.ColumnOrderings(column));
@@ -321,19 +321,19 @@ class $$RemoteExifEntityTableAnnotationComposer
i0.GeneratedColumn<String> get exposureTime => $composableBuilder(
column: $table.exposureTime, builder: (column) => column);
i0.GeneratedColumn<int> get fNumber =>
i0.GeneratedColumn<double> get fNumber =>
$composableBuilder(column: $table.fNumber, builder: (column) => column);
i0.GeneratedColumn<int> get fileSize =>
$composableBuilder(column: $table.fileSize, builder: (column) => column);
i0.GeneratedColumn<int> get focalLength => $composableBuilder(
i0.GeneratedColumn<double> get focalLength => $composableBuilder(
column: $table.focalLength, builder: (column) => column);
i0.GeneratedColumn<int> get latitude =>
i0.GeneratedColumn<double> get latitude =>
$composableBuilder(column: $table.latitude, builder: (column) => column);
i0.GeneratedColumn<int> get longitude =>
i0.GeneratedColumn<double> get longitude =>
$composableBuilder(column: $table.longitude, builder: (column) => column);
i0.GeneratedColumn<int> get iso =>
@@ -416,11 +416,11 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<String?> exposureTime = const i0.Value.absent(),
i0.Value<int?> fNumber = const i0.Value.absent(),
i0.Value<double?> fNumber = const i0.Value.absent(),
i0.Value<int?> fileSize = const i0.Value.absent(),
i0.Value<int?> focalLength = const i0.Value.absent(),
i0.Value<int?> latitude = const i0.Value.absent(),
i0.Value<int?> longitude = const i0.Value.absent(),
i0.Value<double?> focalLength = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<int?> iso = const i0.Value.absent(),
i0.Value<String?> make = const i0.Value.absent(),
i0.Value<String?> model = const i0.Value.absent(),
@@ -462,11 +462,11 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<String?> exposureTime = const i0.Value.absent(),
i0.Value<int?> fNumber = const i0.Value.absent(),
i0.Value<double?> fNumber = const i0.Value.absent(),
i0.Value<int?> fileSize = const i0.Value.absent(),
i0.Value<int?> focalLength = const i0.Value.absent(),
i0.Value<int?> latitude = const i0.Value.absent(),
i0.Value<int?> longitude = const i0.Value.absent(),
i0.Value<double?> focalLength = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<int?> iso = const i0.Value.absent(),
i0.Value<String?> make = const i0.Value.absent(),
i0.Value<String?> model = const i0.Value.absent(),
@@ -622,9 +622,9 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
static const i0.VerificationMeta _fNumberMeta =
const i0.VerificationMeta('fNumber');
@override
late final i0.GeneratedColumn<int> fNumber = i0.GeneratedColumn<int>(
late final i0.GeneratedColumn<double> fNumber = i0.GeneratedColumn<double>(
'f_number', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
type: i0.DriftSqlType.double, requiredDuringInsert: false);
static const i0.VerificationMeta _fileSizeMeta =
const i0.VerificationMeta('fileSize');
@override
@@ -634,21 +634,21 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
static const i0.VerificationMeta _focalLengthMeta =
const i0.VerificationMeta('focalLength');
@override
late final i0.GeneratedColumn<int> focalLength = i0.GeneratedColumn<int>(
'focal_length', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
late final i0.GeneratedColumn<double> focalLength =
i0.GeneratedColumn<double>('focal_length', aliasedName, true,
type: i0.DriftSqlType.double, requiredDuringInsert: false);
static const i0.VerificationMeta _latitudeMeta =
const i0.VerificationMeta('latitude');
@override
late final i0.GeneratedColumn<int> latitude = i0.GeneratedColumn<int>(
late final i0.GeneratedColumn<double> latitude = i0.GeneratedColumn<double>(
'latitude', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
type: i0.DriftSqlType.double, requiredDuringInsert: false);
static const i0.VerificationMeta _longitudeMeta =
const i0.VerificationMeta('longitude');
@override
late final i0.GeneratedColumn<int> longitude = i0.GeneratedColumn<int>(
late final i0.GeneratedColumn<double> longitude = i0.GeneratedColumn<double>(
'longitude', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
type: i0.DriftSqlType.double, requiredDuringInsert: false);
static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso');
@override
late final i0.GeneratedColumn<int> iso = i0.GeneratedColumn<int>(
@@ -853,15 +853,15 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
exposureTime: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']),
fNumber: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}f_number']),
.read(i0.DriftSqlType.double, data['${effectivePrefix}f_number']),
fileSize: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']),
focalLength: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}focal_length']),
.read(i0.DriftSqlType.double, data['${effectivePrefix}focal_length']),
latitude: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}latitude']),
.read(i0.DriftSqlType.double, data['${effectivePrefix}latitude']),
longitude: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}longitude']),
.read(i0.DriftSqlType.double, data['${effectivePrefix}longitude']),
iso: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}iso']),
make: attachedDatabase.typeMapping
@@ -901,11 +901,11 @@ class RemoteExifEntityData extends i0.DataClass
final int? height;
final int? width;
final String? exposureTime;
final int? fNumber;
final double? fNumber;
final int? fileSize;
final int? focalLength;
final int? latitude;
final int? longitude;
final double? focalLength;
final double? latitude;
final double? longitude;
final int? iso;
final String? make;
final String? model;
@@ -964,19 +964,19 @@ class RemoteExifEntityData extends i0.DataClass
map['exposure_time'] = i0.Variable<String>(exposureTime);
}
if (!nullToAbsent || fNumber != null) {
map['f_number'] = i0.Variable<int>(fNumber);
map['f_number'] = i0.Variable<double>(fNumber);
}
if (!nullToAbsent || fileSize != null) {
map['file_size'] = i0.Variable<int>(fileSize);
}
if (!nullToAbsent || focalLength != null) {
map['focal_length'] = i0.Variable<int>(focalLength);
map['focal_length'] = i0.Variable<double>(focalLength);
}
if (!nullToAbsent || latitude != null) {
map['latitude'] = i0.Variable<int>(latitude);
map['latitude'] = i0.Variable<double>(latitude);
}
if (!nullToAbsent || longitude != null) {
map['longitude'] = i0.Variable<int>(longitude);
map['longitude'] = i0.Variable<double>(longitude);
}
if (!nullToAbsent || iso != null) {
map['iso'] = i0.Variable<int>(iso);
@@ -1016,11 +1016,11 @@ class RemoteExifEntityData extends i0.DataClass
height: serializer.fromJson<int?>(json['height']),
width: serializer.fromJson<int?>(json['width']),
exposureTime: serializer.fromJson<String?>(json['exposureTime']),
fNumber: serializer.fromJson<int?>(json['fNumber']),
fNumber: serializer.fromJson<double?>(json['fNumber']),
fileSize: serializer.fromJson<int?>(json['fileSize']),
focalLength: serializer.fromJson<int?>(json['focalLength']),
latitude: serializer.fromJson<int?>(json['latitude']),
longitude: serializer.fromJson<int?>(json['longitude']),
focalLength: serializer.fromJson<double?>(json['focalLength']),
latitude: serializer.fromJson<double?>(json['latitude']),
longitude: serializer.fromJson<double?>(json['longitude']),
iso: serializer.fromJson<int?>(json['iso']),
make: serializer.fromJson<String?>(json['make']),
model: serializer.fromJson<String?>(json['model']),
@@ -1043,11 +1043,11 @@ class RemoteExifEntityData extends i0.DataClass
'height': serializer.toJson<int?>(height),
'width': serializer.toJson<int?>(width),
'exposureTime': serializer.toJson<String?>(exposureTime),
'fNumber': serializer.toJson<int?>(fNumber),
'fNumber': serializer.toJson<double?>(fNumber),
'fileSize': serializer.toJson<int?>(fileSize),
'focalLength': serializer.toJson<int?>(focalLength),
'latitude': serializer.toJson<int?>(latitude),
'longitude': serializer.toJson<int?>(longitude),
'focalLength': serializer.toJson<double?>(focalLength),
'latitude': serializer.toJson<double?>(latitude),
'longitude': serializer.toJson<double?>(longitude),
'iso': serializer.toJson<int?>(iso),
'make': serializer.toJson<String?>(make),
'model': serializer.toJson<String?>(model),
@@ -1068,11 +1068,11 @@ class RemoteExifEntityData extends i0.DataClass
i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> width = const i0.Value.absent(),
i0.Value<String?> exposureTime = const i0.Value.absent(),
i0.Value<int?> fNumber = const i0.Value.absent(),
i0.Value<double?> fNumber = const i0.Value.absent(),
i0.Value<int?> fileSize = const i0.Value.absent(),
i0.Value<int?> focalLength = const i0.Value.absent(),
i0.Value<int?> latitude = const i0.Value.absent(),
i0.Value<int?> longitude = const i0.Value.absent(),
i0.Value<double?> focalLength = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<int?> iso = const i0.Value.absent(),
i0.Value<String?> make = const i0.Value.absent(),
i0.Value<String?> model = const i0.Value.absent(),
@@ -1232,11 +1232,11 @@ class RemoteExifEntityCompanion
final i0.Value<int?> height;
final i0.Value<int?> width;
final i0.Value<String?> exposureTime;
final i0.Value<int?> fNumber;
final i0.Value<double?> fNumber;
final i0.Value<int?> fileSize;
final i0.Value<int?> focalLength;
final i0.Value<int?> latitude;
final i0.Value<int?> longitude;
final i0.Value<double?> focalLength;
final i0.Value<double?> latitude;
final i0.Value<double?> longitude;
final i0.Value<int?> iso;
final i0.Value<String?> make;
final i0.Value<String?> model;
@@ -1300,11 +1300,11 @@ class RemoteExifEntityCompanion
i0.Expression<int>? height,
i0.Expression<int>? width,
i0.Expression<String>? exposureTime,
i0.Expression<int>? fNumber,
i0.Expression<double>? fNumber,
i0.Expression<int>? fileSize,
i0.Expression<int>? focalLength,
i0.Expression<int>? latitude,
i0.Expression<int>? longitude,
i0.Expression<double>? focalLength,
i0.Expression<double>? latitude,
i0.Expression<double>? longitude,
i0.Expression<int>? iso,
i0.Expression<String>? make,
i0.Expression<String>? model,
@@ -1348,11 +1348,11 @@ class RemoteExifEntityCompanion
i0.Value<int?>? height,
i0.Value<int?>? width,
i0.Value<String?>? exposureTime,
i0.Value<int?>? fNumber,
i0.Value<double?>? fNumber,
i0.Value<int?>? fileSize,
i0.Value<int?>? focalLength,
i0.Value<int?>? latitude,
i0.Value<int?>? longitude,
i0.Value<double?>? focalLength,
i0.Value<double?>? latitude,
i0.Value<double?>? longitude,
i0.Value<int?>? iso,
i0.Value<String?>? make,
i0.Value<String?>? model,
@@ -1416,19 +1416,19 @@ class RemoteExifEntityCompanion
map['exposure_time'] = i0.Variable<String>(exposureTime.value);
}
if (fNumber.present) {
map['f_number'] = i0.Variable<int>(fNumber.value);
map['f_number'] = i0.Variable<double>(fNumber.value);
}
if (fileSize.present) {
map['file_size'] = i0.Variable<int>(fileSize.value);
}
if (focalLength.present) {
map['focal_length'] = i0.Variable<int>(focalLength.value);
map['focal_length'] = i0.Variable<double>(focalLength.value);
}
if (latitude.present) {
map['latitude'] = i0.Variable<int>(latitude.value);
map['latitude'] = i0.Variable<double>(latitude.value);
}
if (longitude.present) {
map['longitude'] = i0.Variable<int>(longitude.value);
map['longitude'] = i0.Variable<double>(longitude.value);
}
if (iso.present) {
map['iso'] = i0.Variable<int>(iso.value);
@@ -1,6 +1,8 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
@@ -41,3 +43,36 @@ class IsarExifRepository extends IsarDatabaseRepository {
});
}
}
class DriftRemoteExifRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftRemoteExifRepository(this._db) : super(_db);
Future<ExifInfo?> get(String assetId) {
final query = _db.remoteExifEntity.select()
..where((exif) => exif.assetId.equals(assetId));
return query.map((asset) => asset.toDto()).getSingleOrNull();
}
}
extension on RemoteExifEntityData {
ExifInfo toDto() {
return ExifInfo(
fileSize: fileSize,
description: description,
orientation: orientation,
timeZone: timeZone,
dateTimeOriginal: dateTimeOriginal,
latitude: latitude,
longitude: longitude,
city: city,
state: state,
country: country,
make: make,
model: model,
f: fNumber,
iso: iso,
);
}
}
@@ -1,17 +1,13 @@
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
final remoteAssetRepositoryProvider = Provider<RemoteAssetRepository>(
(ref) => RemoteAssetRepository(ref.watch(driftProvider)),
);
class RemoteAssetRepository extends DriftDatabaseRepository {
class DriftRemoteAssetRepository extends DriftDatabaseRepository {
final Drift _db;
const RemoteAssetRepository(this._db) : super(_db);
const DriftRemoteAssetRepository(this._db) : super(_db);
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
return _db.batch((batch) async {
@@ -36,4 +32,19 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
}
});
}
Future<void> updateLocation(List<String> ids, LatLng location) {
return _db.batch((batch) async {
for (final id in ids) {
batch.update(
_db.remoteExifEntity,
RemoteExifEntityCompanion(
latitude: Value(location.latitude),
longitude: Value(location.longitude),
),
where: (e) => e.assetId.equals(id),
);
}
});
}
}
@@ -1,16 +1,54 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class EditLocationActionButton extends ConsumerWidget {
const EditLocationActionButton({super.key});
final ActionSource source;
const EditLocationActionButton({super.key, required this.source});
_onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
final result =
await ref.read(actionProvider.notifier).editLocation(source, context);
if (result == null) {
return;
}
ref.read(multiSelectProvider.notifier).reset();
final successMessage = 'edit_location_action_prompt'.t(
context: context,
args: {'count': result.count.toString()},
);
if (context.mounted) {
ImmichToast.show(
context: context,
msg: result.success
? successMessage
: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: result.success ? ToastType.success : ToastType.error,
);
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
iconData: Icons.edit_location_alt_outlined,
label: "control_bottom_app_bar_edit_location".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}
@@ -42,7 +42,7 @@ class HomeBottomAppBar extends ConsumerWidget {
? const TrashActionButton()
: const DeletePermanentActionButton(),
const EditDateTimeActionButton(),
const EditLocationActionButton(),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(
source: ActionSource.timeline,
),
@@ -172,4 +172,26 @@ class ActionNotifier extends Notifier<void> {
);
}
}
Future<ActionResult?> editLocation(
ActionSource source,
BuildContext context,
) async {
final ids = _getIdsForSource<RemoteAsset>(source);
try {
final isEdited = await _service.editLocation(ids, context);
if (!isEdited) {
return null;
}
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to edit location for assets', error, stack);
return ActionResult(
count: ids.length,
success: false,
error: error.toString(),
);
}
}
}
@@ -1,7 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final localAssetRepository = Provider<DriftLocalAssetRepository>(
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
);
final remoteAssetRepository = Provider<DriftRemoteAssetRepository>(
(ref) => DriftRemoteAssetRepository(ref.watch(driftProvider)),
);
@@ -8,3 +8,7 @@ part 'exif.provider.g.dart';
@Riverpod(keepAlive: true)
IsarExifRepository exifRepository(Ref ref) =>
IsarExifRepository(ref.watch(isarProvider));
final remoteExifRepository = Provider<DriftRemoteExifRepository>(
(ref) => DriftRemoteExifRepository(ref.watch(driftProvider)),
);
@@ -3,6 +3,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider(
@@ -65,6 +66,19 @@ class AssetApiRepository extends ApiRepository {
);
}
Future<void> updateLocation(
List<String> ids,
LatLng location,
) async {
return _api.updateAssets(
AssetBulkUpdateDto(
ids: ids,
latitude: location.latitude,
longitude: location.longitude,
),
);
}
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
+48 -3
View File
@@ -2,23 +2,34 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/location_picker.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final actionServiceProvider = Provider<ActionService>(
(ref) => ActionService(
ref.watch(assetApiRepositoryProvider),
ref.watch(remoteAssetRepositoryProvider),
ref.watch(remoteAssetRepository),
ref.watch(remoteExifRepository),
),
);
class ActionService {
final AssetApiRepository _assetApiRepository;
final RemoteAssetRepository _remoteAssetRepository;
final DriftRemoteAssetRepository _remoteAssetRepository;
final DriftRemoteExifRepository _remoteExifRepository;
const ActionService(this._assetApiRepository, this._remoteAssetRepository);
const ActionService(
this._assetApiRepository,
this._remoteAssetRepository,
this._remoteExifRepository,
);
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
context.pushRoute(
@@ -81,4 +92,38 @@ class ActionService {
AssetVisibility.timeline,
);
}
Future<bool> editLocation(
List<String> remoteIds,
BuildContext context,
) async {
LatLng? initialLatLng;
if (remoteIds.length == 1) {
final exif = await _remoteExifRepository.get(remoteIds[0]);
if (exif?.latitude != null && exif?.longitude != null) {
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
}
}
final location = await showLocationPicker(
context: context,
initialLatLng: initialLatLng,
);
if (location == null) {
return false;
}
await _assetApiRepository.updateLocation(
remoteIds,
location,
);
await _remoteAssetRepository.updateLocation(
remoteIds,
location,
);
return true;
}
}
+10 -10
View File
@@ -56,21 +56,21 @@ class SyncAssetExifV1 {
String? exposureTime;
int? fNumber;
double? fNumber;
int? fileSizeInByte;
int? focalLength;
double? focalLength;
int? fps;
double? fps;
int? iso;
int? latitude;
double? latitude;
String? lensModel;
int? longitude;
double? longitude;
String? make;
@@ -293,14 +293,14 @@ class SyncAssetExifV1 {
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
fNumber: mapValueOfType<int>(json, r'fNumber'),
fNumber: (mapValueOfType<num>(json, r'fNumber'))?.toDouble(),
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
focalLength: mapValueOfType<int>(json, r'focalLength'),
fps: mapValueOfType<int>(json, r'fps'),
focalLength: (mapValueOfType<num>(json, r'focalLength'))?.toDouble(),
fps: (mapValueOfType<num>(json, r'fps'))?.toDouble(),
iso: mapValueOfType<int>(json, r'iso'),
latitude: mapValueOfType<int>(json, r'latitude'),
latitude: (mapValueOfType<num>(json, r'latitude'))?.toDouble(),
lensModel: mapValueOfType<String>(json, r'lensModel'),
longitude: mapValueOfType<int>(json, r'longitude'),
longitude: (mapValueOfType<num>(json, r'longitude'))?.toDouble(),
make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'),
modifyDate: mapDateTime(json, r'modifyDate', r''),
+2 -2
View File
@@ -28,11 +28,11 @@ function dart {
function typescript {
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
}
# requires server to be built
npm run sync:open-api --prefix=../server
(cd .. && pnpm --filter immich install && pnpm --filter immich build && pnpm --filter immich sync:open-api)
if [[ $1 == 'dart' ]]; then
dart
+10 -5
View File
@@ -13615,36 +13615,41 @@
"type": "string"
},
"fNumber": {
"format": "double",
"nullable": true,
"type": "integer"
"type": "number"
},
"fileSizeInByte": {
"nullable": true,
"type": "integer"
},
"focalLength": {
"format": "double",
"nullable": true,
"type": "integer"
"type": "number"
},
"fps": {
"format": "double",
"nullable": true,
"type": "integer"
"type": "number"
},
"iso": {
"nullable": true,
"type": "integer"
},
"latitude": {
"format": "double",
"nullable": true,
"type": "integer"
"type": "number"
},
"lensModel": {
"nullable": true,
"type": "string"
},
"longitude": {
"format": "double",
"nullable": true,
"type": "integer"
"type": "number"
},
"make": {
"nullable": true,
@@ -206,7 +206,7 @@ class {{{classname}}} {
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
{{/isNumber}}
{{#isDouble}}
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(),
{{/isDouble}}
{{^isDouble}}
{{^isNumber}}
@@ -1,5 +1,5 @@
--- native_class.mustache 2024-09-19 11:41:07.855683995 -0400
+++ native_class_temp.mustache 2024-09-19 11:41:57.113249395 -0400
--- native_class.mustache 2025-07-01 08:29:23.968133163 +0800
+++ native_class_temp.mustache 2025-07-01 08:29:44.225850583 +0800
@@ -91,14 +91,14 @@
{{/isDateTime}}
{{#isNullable}}
@@ -44,7 +44,7 @@
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
{{/isNumber}}
+ {{#isDouble}}
+ {{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
+ {{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(),
+ {{/isDouble}}
+ {{^isDouble}}
{{^isNumber}}
+1 -1
View File
@@ -5,7 +5,7 @@ A TypeScript SDK for interfacing with the [Immich](https://immich.app/) API.
## Install
```bash
npm i --save @immich/sdk
pnpm i --save @immich/sdk
```
## Usage
-57
View File
@@ -1,57 +0,0 @@
{
"name": "@immich/sdk",
"version": "1.135.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.135.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.15.33",
"typescript": "^5.3.3"
}
},
"node_modules/@oazapfts/runtime": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.4.tgz",
"integrity": "sha512-7t6C2shug/6tZhQgkCa532oTYBLEnbASV/i1SG1rH2GB4h3aQQujYciYSPT92hvN4IwTe8S2hPkN/6iiOyTlCg==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz",
"integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
+2 -1
View File
@@ -29,5 +29,6 @@
},
"volta": {
"node": "22.17.0"
}
},
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "immich-monorepo",
"version": "0.0.1",
"description": "monorepo for immich and friends",
"private": true,
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417",
"engines": {
"pnpm": ">=10.0.0"
}
}
+26148
View File
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
packages:
- cli
- docs
- e2e
- open-api/typescript-sdk
- server
- web
- .github
dedupePeerDependents: false
ignoredBuiltDependencies:
- canvas
- es5-ext
- esbuild
- '@nestjs/core'
- '@scarf/scarf'
- '@swc/core'
- bcrypt
- cpu-features
- msgpackr-extract
- protobufjs
- ssh2
- utimes
onlyBuiltDependencies:
- sharp
- '@tailwindcss/oxide'
overrides:
"canvas": "2.11.2"
"sharp": "^0.34.2"
'@img/sharp-darwin-arm64': '-'
'@img/sharp-darwin-x64': '-'
'@img/sharp-libvips-darwin-arm64': '-'
'@img/sharp-libvips-darwin-x64': '-'
'@img/sharp-libvips-linux-arm': '-'
# '@img/sharp-libvips-linux-arm64': '-'
'@img/sharp-libvips-linux-ppc64': '-'
'@img/sharp-libvips-linux-s390x': '-'
# '@img/sharp-libvips-linux-x64': '-'
'@img/sharp-libvips-linuxmusl-arm64': '-'
# '@img/sharp-libvips-linuxmusl-x64': '-'
# '@img/sharp-linux-arm': '-'
'@img/sharp-linux-arm64': '-'
'@img/sharp-linux-s390x': '-'
# '@img/sharp-linux-x64': '-'
'@img/sharp-linuxmusl-arm64': '-'
# '@img/sharp-linuxmusl-x64': '-'
'@img/sharp-wasm32': '-'
'@img/sharp-win32-arm64': '-'
'@img/sharp-win32-ia32': '-'
'@img/sharp-win32-x64': '-'
packageExtensions:
# these packages use tslib, but do not declare it as a dependency
nestjs-kysely:
dependencies:
tslib: '*'
nestjs-otel:
dependencies:
tslib: '*'
'@photo-sphere-viewer/equirectangular-video-adapter':
dependencies:
three: '*'
'@photo-sphere-viewer/video-plugin':
dependencies:
three: '*'
sharp:
dependencies:
node-addon-api: '*'
node-gyp: '*'
'@immich/ui':
dependencies:
tailwindcss: '^4.1.11'
'tailwind-variants':
dependencies:
tailwindcss: '^4.1.11'
preferWorkspacePackages: true
shamefullyHoist: true
+148 -64
View File
@@ -4,30 +4,75 @@ FROM ghcr.io/immich-app/base-server-dev:202505131114@sha256:cf4507bbbf307e9b6d8e
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./
COPY server/patches ./patches
RUN npm ci && \
# exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
# they're marked as optional dependencies, so we need to copy them manually after pruning
rm -rf node_modules/@img/sharp-libvips* && \
rm -rf node_modules/@img/sharp-linuxmusl-x64
# exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
# they're marked as optional dependencies, so we need to copy them manually after pruning
rm -rf node_modules/@img/sharp-libvips* && \
rm -rf node_modules/@img/sharp-linuxmusl-x64
ENV PATH="${PATH}:/usr/src/app/bin" \
IMMICH_ENV=development \
NVIDIA_DRIVER_CAPABILITIES=all \
NVIDIA_VISIBLE_DEVICES=all
IMMICH_ENV=development \
NVIDIA_DRIVER_CAPABILITIES=all \
NVIDIA_VISIBLE_DEVICES=all \
COREPACK_ENABLE_AUTO_PIN=0 \
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
npm_config_devdir=/buildcache/node_gyp
RUN corepack enable && \
corepack install -g pnpm && \
apt-get install --no-install-recommends -yqq tini
RUN mkdir -p /buildcache/pnpm_store && \
chown -R node:node /buildcache && \
mkdir -p /usr/local/etc && \
echo "store-dir=/buildcache/pnpm_store" >> /usr/local/etc/npmrc
RUN rm -rf /usr/src/app && \
mkdir -p /usr/src/app && \
chown node:node /usr/src/app
USER node
WORKDIR /usr/src/app
COPY --chown=node:node . .
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm fetch
ENTRYPOINT ["tini", "--", "/bin/sh"]
FROM dev AS dev-docker
WORKDIR /usr/src/app
VOLUME /usr/src/app/node_modules
RUN pnpm fetch && make setup-dev
FROM dev AS dev-container-server
USER root
RUN pnpm fetch
# Remove app dir from dev container
RUN rm -rf /usr/src/app
RUN apt-get update && \
apt-get install sudo inetutils-ping openjdk-11-jre-headless \
vim nano \
-y --no-install-recommends --fix-missing
RUN usermod -aG sudo node
RUN echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN mkdir -p /workspaces/immich
RUN chown node -R /workspaces
COPY --chown=node:node --chmod=777 ../.devcontainer/server/*.sh /immich-devcontainer/
RUN apt-get update && \
apt-get install sudo inetutils-ping openjdk-11-jre-headless \
vim nano -y --no-install-recommends --fix-missing
RUN usermod -aG sudo node && \
echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN sudo mkdir -p /workspaces/immich && \
sudo chown node -R /workspaces && \
sudo mkdir /immich-devcontainer && \
sudo chown node -R /immich-devcontainer
COPY --chmod=777 \
../.devcontainer/server/*.sh \
/immich-devcontainer/
WORKDIR /workspaces/immich
USER node
COPY --chown=node:node .. /tmp/create-dep-cache/
@@ -36,17 +81,16 @@ RUN make ci-all && rm -rf /tmp/create-dep-cache
FROM dev-container-server AS dev-container-mobile
USER root
# USER root
# Enable multiarch for arm64 if necessary
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
dpkg --add-architecture amd64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64; \
fi
sudo dpkg --add-architecture amd64 && \
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64; \
fi
# Flutter SDK
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
@@ -56,15 +100,13 @@ 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 node ${FLUTTER_HOME}
RUN sudo mkdir -p ${FLUTTER_HOME} \
&& sudo 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 \
&& sudo tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
&& sudo rm flutter.tar.xz \
&& sudo chown -R node ${FLUTTER_HOME}
USER node
RUN sudo apt-get update \
&& wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
RUN wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
&& 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 -y
@@ -73,49 +115,89 @@ COPY --chmod=777 ../.devcontainer/mobile/container-mobile-post-create.sh /immich
RUN dart --disable-analytics
FROM dev AS prod
# server production build
FROM dev AS server-prod
COPY server .
RUN npm run build
RUN npm prune --omit=dev --omit=optional
COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
WORKDIR /usr/src/app/server
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm build
# web build
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS web
FROM dev AS sdk-prod
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
RUN npm ci
COPY open-api/typescript-sdk/ ./
RUN npm run build
WORKDIR /usr/src/app/open-api/typescript-sdk
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm build
WORKDIR /usr/src/app
COPY web/package*.json web/svelte.config.js ./
RUN npm ci
COPY web ./
COPY i18n ../i18n
RUN npm run build
# web production build
FROM dev AS web-prod
COPY --from=sdk-prod /usr/src/app/open-api/typescript-sdk /usr/src/app/open-api/typescript-sdk
RUN rm -rf /usr/src/app/open-api/typescript-sdk/node_modules
WORKDIR /usr/src/app/web
# USER root
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm rebuild && pnpm build
FROM dev AS cli-prod
WORKDIR /usr/src/app/web
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm exec svelte-kit sync
WORKDIR /usr/src/app/open-api/typescript-sdk
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm build
WORKDIR /usr/src/app/cli
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --frozen-lockfile && \
pnpm build
# prod build
FROM ghcr.io/immich-app/base-server-prod:202505061115@sha256:9971d3a089787f0bd01f4682141d3665bcf5efb3e101a88e394ffd25bee4eedb
FROM ghcr.io/midzelis/base-images/base-server-prod:latest
RUN corepack enable && \
corepack install -g pnpm
WORKDIR /usr/src/app
ENV NODE_ENV=production \
NVIDIA_DRIVER_CAPABILITIES=all \
NVIDIA_VISIBLE_DEVICES=all
COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin
COPY --from=web /usr/src/app/build /build/www
COPY server/resources resources
COPY server/package.json server/package-lock.json ./
COPY server/start*.sh ./
COPY "docker/scripts/get-cpus.sh" ./
RUN npm install -g @immich/cli && npm cache clean --force
NVIDIA_DRIVER_CAPABILITIES=all \
NVIDIA_VISIBLE_DEVICES=all \
npm_config_devdir=/buildcache/node_gyp \
COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN mkdir -p /buildcache/pnpm_store && \
chown -R node:node /buildcache && \
mkdir -p /usr/local/etc && \
echo "store-dir=/buildcache/pnpm_store" >> /usr/local/etc/npmrc && \
mkdir -p /usr/src/app/upload && \
chown -R node:node /usr/src/app && \
chmod 755 /usr/src/app
COPY --chown=node:node --from=server-prod /usr/src/app/server/dist ./dist
COPY --chown=node:node --from=server-prod /usr/src/app/server/bin ./bin
COPY --chown=node:node --from=server-prod /usr/src/app/server/package.json ./
COPY --chown=node:node --from=web-prod /usr/src/app/node_modules/ ./node_modules
COPY --chown=node:node --from=web-prod /usr/src/app/web/build /build/www
COPY --chown=node:node --from=cli-prod /usr/src/app/cli/dist ./cli
COPY --chown=node:node server/resources ./resources/
COPY --chown=node:node server/start*.sh \
docker/scripts/get-cpus.sh \
pnpm-workspace.yaml \
pnpm-lock.yaml ./
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
USER node
RUN --mount=type=cache,id=pnpm,target=/buildcache,uid=1000,gid=1000 \
pnpm install --prod --no-optional && \
echo '#!/usr/bin/env node' > /usr/src/app/bin/immich && \
echo 'require("../cli/index.js");' >> /usr/src/app/bin/immich && \
chmod +x /usr/src/app/bin/immich
ENV PATH="${PATH}:/usr/src/app/bin"
ARG BUILD_ID
@@ -133,6 +215,8 @@ ENV IMMICH_SOURCE_REF=${BUILD_SOURCE_REF}
ENV IMMICH_SOURCE_COMMIT=${BUILD_SOURCE_COMMIT}
ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE_COMMIT}
USER root
VOLUME /usr/src/app/upload
EXPOSE 2283
ENTRYPOINT ["tini", "--", "/bin/bash"]
+4 -1
View File
@@ -1,3 +1,6 @@
#!/usr/bin/env bash
node /usr/src/app/node_modules/.bin/nest start --debug "0.0.0.0:9230" --watch -- "$@"
cd /usr/src/app || exit
FROZEN=1 OFFLINE=1 make setup-dev
cd /usr/src/app/server || exit
pnpm exec nest start --debug "0.0.0.0:9230" --watch -- "$@"
-18496
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -176,5 +176,6 @@
},
"overrides": {
"sharp": "^0.34.2"
}
},
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
}
+5 -5
View File
@@ -115,9 +115,9 @@ export class SyncAssetExifV1 {
dateTimeOriginal!: Date | null;
modifyDate!: Date | null;
timeZone!: string | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'number', format: 'double' })
latitude!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'number', format: 'double' })
longitude!: number | null;
projectionType!: string | null;
city!: string | null;
@@ -126,9 +126,9 @@ export class SyncAssetExifV1 {
make!: string | null;
model!: string | null;
lensModel!: string | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'number', format: 'double' })
fNumber!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'number', format: 'double' })
focalLength!: number | null;
@ApiProperty({ type: 'integer' })
iso!: number | null;
@@ -136,7 +136,7 @@ export class SyncAssetExifV1 {
profileDescription!: string | null;
@ApiProperty({ type: 'integer' })
rating!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'number', format: 'double' })
fps!: number | null;
}
@@ -2,9 +2,7 @@ import React from 'react';
import { Button, ButtonProps } from '@react-email/components';
interface ImmichButtonProps extends ButtonProps {}
export const ImmichButton = ({ children, ...props }: ImmichButtonProps) => (
export const ImmichButton = ({ children, ...props }: ButtonProps) => (
<Button
{...props}
className="py-3 px-8 border bg-immich-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 font-bold uppercase"
+17 -3
View File
@@ -1,11 +1,25 @@
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e
ENV COREPACK_ENABLE_AUTO_PIN=0 \
COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable && corepack install -g pnpm && \
apk add --no-cache tini make && \
mkdir -p /pnpm/store && \
chown node:node -R /pnpm && \
mkdir -p /usr/local/etc && \
echo "store-dir=/pnpm/store" >> /usr/local/etc/npmrc
RUN apk add --no-cache tini
USER node
WORKDIR /usr/src/app
COPY --chown=node:node package*.json ./
RUN npm ci
COPY --chown=node:node . .
RUN pnpm fetch && make setup-dev
WORKDIR /usr/src/app/web
ENV CHOKIDAR_USEPOLLING=true
EXPOSE 24678
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--", "/bin/sh"]
+9 -13
View File
@@ -1,21 +1,17 @@
#!/usr/bin/env sh
TYPESCRIPT_SDK=/usr/src/open-api/typescript-sdk
npm --prefix "$TYPESCRIPT_SDK" install
npm --prefix "$TYPESCRIPT_SDK" run build
echo "Setup dev env"
(cd /usr/src/app && FROZEN=1 OFFLINE=1 make setup-dev)
COUNT=0
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}"
until wget --spider --quiet "${UPSTREAM}/api/server/config" > /dev/null 2>&1; do
if [ $((COUNT % 10)) -eq 0 ]; then
echo "Waiting for $UPSTREAM to start..."
fi
COUNT=$((COUNT + 1))
sleep 1
until wget --spider --quiet "${UPSTREAM}/api/server/config" >/dev/null 2>&1; do
if [ $((COUNT % 10)) -eq 0 ]; then
echo "Waiting for $UPSTREAM to start..."
fi
COUNT=$((COUNT + 1))
sleep 1
done
echo "Connected to $UPSTREAM"
node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000
pnpm exec vite dev --host 0.0.0.0 --port 3000
-10929
View File
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -5,25 +5,24 @@
"type": "module",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
"build": "vite build",
"build": "svelte-kit sync && vite build",
"build:stats": "BUILD_STATS=true vite build",
"package": "svelte-kit package",
"preview": "vite preview",
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte",
"check:typescript": "tsc --noEmit",
"check:typescript": "svelte-kit sync && tsc --noEmit",
"check:watch": "npm run check:svelte -- --watch",
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript",
"check:all": "npm run check:code && npm run test:cov",
"lint": "eslint . --max-warnings 0",
"lint:p": "eslint-p . --max-warnings 0 --concurrency=4",
"lint:fix": "npm run lint -- --fix",
"lint": "svelte-kit sync && eslint . --max-warnings 0",
"lint:p": "svelte-kit sync && eslint-p . --max-warnings 0 --concurrency=4",
"lint:fix": "svelte-kit sync && npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write . && npm run format:i18n",
"format:i18n": "npx --yes sort-json ../i18n/*.json",
"test": "vitest --run",
"test:cov": "vitest --coverage",
"test:watch": "vitest dev",
"prepare": "svelte-kit sync"
"test:watch": "vitest dev"
},
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -39,7 +38,9 @@
"@zoom-image/svelte": "^0.3.0",
"dom-to-image": "^2.6.0",
"fabric": "^6.5.4",
"geojson": "^0.5.0",
"handlebars": "^4.7.8",
"happy-dom": "^18.0.1",
"intl-messageformat": "^10.7.11",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
@@ -104,5 +105,6 @@
},
"volta": {
"node": "22.17.0"
}
},
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
}