Compare commits

..

10 Commits

Author SHA1 Message Date
shenlong-tanwen
2f1ec5b724 fix: prune stale assets 2025-10-02 17:32:43 +05:30
renovate[bot]
00ce6354f0 chore(deps): update node.js to v22.20.0 (#22496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 09:17:40 +00:00
Kenny at FUTO
9b938d8954 feat(docs): add one-click install docs page (#22553)
* feat(docs): add one-click install docs page
2025-10-01 17:24:21 -05:00
github-actions
e4234af3b3 chore: version v2.0.0 2025-10-01 21:19:34 +00:00
Jason Rasmussen
1618902ebd chore: remove warnings (#22549) 2025-10-01 21:14:51 +00:00
Alex
83e783bbc2 chore: update readme (#22548)
Removed disclaimer section and replaced it with a warning.
2025-10-01 16:13:38 -05:00
Jason Rasmussen
7dbdc7a635 chore: remove warning banner (#22547) 2025-10-01 21:06:22 +00:00
Jason Rasmussen
7a4bfc21c9 fix(docs): open graph tags (#22542) 2025-10-01 16:30:52 +00:00
Guillermo
b3f38301bf fix: missing email button padding (#22529)
Signed-off-by: Guillermo Guirao Aguilar <ggaguilar@gmail.com>
2025-10-01 09:03:22 -05:00
Alex
1f7201fbd3 fix(server): Revert update libmimalloc path (#22345) (#22526)
* Revert "fix(server): update libmimalloc path (#22345)"

This reverts commit 38226fd240.

* add comments
2025-10-01 13:58:24 +00:00
36 changed files with 125 additions and 316 deletions

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -38,12 +38,11 @@
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
</p>
## Disclaimer
- ⚠️ The project is under **very active** development.
- ⚠️ Expect bugs and breaking changes.
- ⚠️ **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!
> [!WARNING]
> ⚠️ 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]
> You can find the main documentation, including installation guides, at https://immich.app/.

View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.94",
"version": "2.2.95",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -68,6 +68,6 @@
"micromatch": "^4.0.8"
},
"volta": {
"node": "22.19.0"
"node": "22.20.0"
}
}

View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -0,0 +1,32 @@
---
sidebar_position: 65
---
# One-Click [Cloud Service]
:::note
This version of Immich is provided via cloud service provider's one-click marketplaces. Hosting costs are set by the cloud service providers.
Support for these are provided by the individual cloud service providers.
**Please report issues to the corresponding [Github Repository][github].**
:::
## Installation
Simply goto the providers marketplace and choose Immich, then follow the provided instructions.
## One-Click Immich marketplace providers
### DigitalOcean
https://marketplace.digitalocean.com/apps/immich
### Vultr
https://www.vultr.com/marketplace/apps/immich
## Issues
For issues, open an issue on the associated [GitHub Repository][github].
[github]: https://github.com/imagegenius/docker-immich/

View File

@@ -4,9 +4,7 @@ sidebar_position: 95
# Upgrading
:::danger Read the release notes
Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. You should read the release notes prior to updating and take special care when using automated tools like [Watchtower][watchtower].
:::tip Breaking changes
You can see versions that had breaking changes [here][breaking].
:::

View File

@@ -1,7 +1,6 @@
Now that you have imported some pictures, you should setup server backups to preserve your memories.
You can do so by following our [backup guide](/administration/backup-and-restore.md).
:::danger
Immich is still under heavy development _and_ handles very important data.
It is essential that you set up good backups, and test them.
:::info
A 3-2-1 backup strategy is still crucial. The team has the responsibility to ensure that the application doesnt cause loss of your precious memories; however, we cannot guarantee that hard drives will not fail, or an electrical event causes unexpected shutdown of your server/system, leading to data loss. Therefore, we still encourage users to follow best practices when safeguarding their data. Keep multiple copies of your most precious data: at least two local copies and one copy offsite in cold storage.
:::

View File

@@ -7,7 +7,7 @@ const prism = require('prism-react-renderer');
const config = {
title: 'Immich',
tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone',
url: 'https://immich.app',
url: 'https://docs.immich.app',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
@@ -65,11 +65,6 @@ const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
announcementBar: {
id: 'site_announcement_immich',
content: `⚠️ The project is under <strong>very active</strong> development. Expect bugs and changes. Do not use it as <strong>the only way</strong> to store your photos and videos!`,
isCloseable: false,
},
docs: {
sidebar: {
autoCollapseCategories: false,

View File

@@ -57,6 +57,6 @@
"node": ">=20"
},
"volta": {
"node": "22.19.0"
"node": "22.20.0"
}
}

View File

@@ -1,4 +1,8 @@
[
{
"label": "v2.0.0",
"url": "https://docs.v2.0.0.archive.immich.app"
},
{
"label": "v1.144.1",
"url": "https://docs.v1.144.1.archive.immich.app"

View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.144.1",
"version": "2.0.0",
"description": "",
"main": "index.js",
"type": "module",
@@ -52,6 +52,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.19.0"
"node": "22.20.0"
}
}

View File

@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "1.144.1"
version = "2.0.0"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.10,<4.0"

View File

@@ -1,5 +1,5 @@
[tools]
node = "22.19.0"
node = "22.20.0"
flutter = "3.35.4"
pnpm = "10.15.1"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3019,
"android.injected.version.name" => "1.144.1",
"android.injected.version.code" => 3020,
"android.injected.version.name" => "2.0.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.144.1"
version_number: "2.0.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -32,9 +32,9 @@ import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/wm_executor.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
class BackgroundWorkerFgService {
final BackgroundWorkerFgHostApi _foregroundHostApi;
@@ -93,7 +93,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
await Future.wait(
[
loadTranslations(),
workerManagerPatch.init(dynamicSpawning: true),
workerManager.init(dynamicSpawning: true),
_ref?.read(authServiceProvider).setOpenApiServiceEndpoint(),
// Initialize the file downloader
FileDownloader().configure(
@@ -198,7 +198,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
_cancellationToken.cancel();
_logger.info("Cleaning up background worker");
final cleanupFutures = [
workerManagerPatch.dispose().catchError((_) async {
workerManager.dispose().catchError((_) async {
// Discard any errors on the dispose call
return;
}),

View File

@@ -130,9 +130,9 @@ class SyncStreamService {
// to acknowledge that the client has processed all the backfill events
case SyncEntityType.syncAckV1:
return;
// No-op. SyncCompleteV1 is used to signal the completion of the sync process
// SyncCompleteV1 is used to signal the completion of the sync process. Cleanup stale assets and signal completion
case SyncEntityType.syncCompleteV1:
return;
return _syncStreamRepository.pruneAssets();
// Request to reset the client state. Clear everything related to remote entities
case SyncEntityType.syncResetV1:
return _syncStreamRepository.reset();

View File

@@ -591,6 +591,40 @@ class SyncStreamRepository extends DriftDatabaseRepository {
rethrow;
}
}
Future<void> pruneAssets() async {
try {
await _db.transaction(() async {
final authQuery = _db.authUserEntity.selectOnly()
..addColumns([_db.authUserEntity.id])
..limit(1);
final currentUserId = await authQuery.map((row) => row.read(_db.authUserEntity.id)).getSingleOrNull();
if (currentUserId == null) {
_logger.warning('No authenticated user found during pruneAssets. Skipping asset pruning.');
return;
}
final partnerQuery = _db.partnerEntity.selectOnly()
..addColumns([_db.partnerEntity.sharedById])
..where(_db.partnerEntity.sharedWithId.equals(currentUserId));
final partnerIds = await partnerQuery.map((row) => row.read(_db.partnerEntity.sharedById)).get();
final validUsers = {currentUserId, ...partnerIds.nonNulls};
// Asset is not owned by the current user or any of their partners and is not part of any (shared) album
// Likely a stale asset that was previously shared but has been removed
await _db.remoteAssetEntity.deleteWhere((asset) {
return asset.ownerId.isNotIn(validUsers) &
asset.id.isNotInQuery(
_db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]),
);
});
});
} catch (error, stack) {
_logger.severe('Error: pruneAssets', error, stack);
// We do not rethrow here as this is a client-only cleanup and should not affect the sync process
}
}
}
extension on AssetTypeEnum {

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
@@ -39,10 +38,10 @@ import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/utils/licenses.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:immich_mobile/wm_executor.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:logging/logging.dart';
import 'package:timezone/data/latest.dart';
import 'package:worker_manager/worker_manager.dart';
void main() async {
ImmichWidgetsBinding();
@@ -51,7 +50,7 @@ void main() async {
await Bootstrap.initDomain(isar, drift, logDb);
await initApp();
// Warm-up isolate pool for worker manager
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
await workerManager.init(dynamicSpawning: true);
await migrateDatabaseIfNeeded(isar, drift);
HttpSSLOptions.apply();

View File

@@ -11,7 +11,6 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/wm_executor.dart';
import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
@@ -32,7 +31,7 @@ Cancelable<T?> runInIsolateGentle<T>({
throw const InvalidIsolateUsageException();
}
return workerManagerPatch.executeGentle((cancelledChecker) async {
return workerManager.executeGentle((cancelledChecker) async {
T? result;
await runZonedGuarded(
() async {

View File

@@ -1,251 +0,0 @@
// part of 'package:worker_manager/worker_manager.dart';
// ignore_for_file: implementation_imports, avoid_print
import 'dart:async';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:worker_manager/src/number_of_processors/processors_io.dart';
import 'package:worker_manager/src/worker/worker.dart';
import 'package:worker_manager/worker_manager.dart';
final workerManagerPatch = _Executor();
// [-2^54; 2^53] is compatible with dart2js, see core.int doc
const _minId = -9007199254740992;
const _maxId = 9007199254740992;
class Mixinable<T> {
late final itSelf = this as T;
}
mixin _ExecutorLogger on Mixinable<_Executor> {
var log = false;
@mustCallSuper
void init() {
logMessage("${itSelf._isolatesCount} workers have been spawned and initialized");
}
void logTaskAdded<R>(String uid) {
logMessage("added task with number $uid");
}
@mustCallSuper
void dispose() {
logMessage("worker_manager have been disposed");
}
@mustCallSuper
void _cancel(Task task) {
logMessage("Task ${task.id} have been canceled");
}
void logMessage(String message) {
if (log) print(message);
}
}
class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
final _queue = PriorityQueue<Task>();
final _pool = <Worker>[];
var _nextTaskId = _minId;
var _dynamicSpawning = false;
var _isolatesCount = numberOfProcessors;
@override
Future<void> init({int? isolatesCount, bool? dynamicSpawning}) async {
if (_pool.isNotEmpty) {
print("worker_manager already warmed up, init is ignored. Dispose before init");
return;
}
if (isolatesCount != null) {
if (isolatesCount < 0) {
throw Exception("isolatesCount must be greater than 0");
}
_isolatesCount = isolatesCount;
}
_dynamicSpawning = dynamicSpawning ?? false;
await _ensureWorkersInitialized();
super.init();
}
@override
Future<void> dispose() async {
_queue.clear();
for (final worker in _pool) {
worker.kill();
}
_pool.clear();
super.dispose();
}
Cancelable<R> execute<R>(Execute<R> execution, {WorkPriority priority = WorkPriority.immediately}) {
return _createCancelable<R>(execution: execution, priority: priority);
}
Cancelable<R> executeNow<R>(ExecuteGentle<R> execution) {
final task = TaskGentle<R>(
id: "",
workPriority: WorkPriority.immediately,
execution: execution,
completer: Completer<R>(),
);
Future<void> run() async {
try {
final result = await execution(() => task.canceled);
task.complete(result, null, null);
} catch (error, st) {
task.complete(null, error, st);
}
}
run();
return Cancelable(completer: task.completer, onCancel: () => _cancel(task));
}
Cancelable<R> executeWithPort<R, T>(
ExecuteWithPort<R> execution, {
WorkPriority priority = WorkPriority.immediately,
required void Function(T value) onMessage,
}) {
return _createCancelable<R>(
execution: execution,
priority: priority,
onMessage: (message) => onMessage(message as T),
);
}
Cancelable<R> executeGentle<R>(ExecuteGentle<R> execution, {WorkPriority priority = WorkPriority.immediately}) {
return _createCancelable<R>(execution: execution, priority: priority);
}
Cancelable<R> executeGentleWithPort<R, T>(
ExecuteGentleWithPort<R> execution, {
WorkPriority priority = WorkPriority.immediately,
required void Function(T value) onMessage,
}) {
return _createCancelable<R>(
execution: execution,
priority: priority,
onMessage: (message) => onMessage(message as T),
);
}
void _createWorkers() {
for (var i = 0; i < _isolatesCount; i++) {
_pool.add(Worker());
}
}
Future<void> _initializeWorkers() async {
await Future.wait(_pool.map((e) => e.initialize()));
}
Cancelable<R> _createCancelable<R>({
required Function execution,
WorkPriority priority = WorkPriority.immediately,
void Function(Object value)? onMessage,
}) {
if (_nextTaskId + 1 == _maxId) {
_nextTaskId = _minId;
}
final id = _nextTaskId.toString();
_nextTaskId++;
late final Task<R> task;
final completer = Completer<R>();
if (execution is Execute<R>) {
task = TaskRegular<R>(id: id, workPriority: priority, execution: execution, completer: completer);
} else if (execution is ExecuteWithPort<R>) {
task = TaskWithPort<R>(
id: id,
workPriority: priority,
execution: execution,
completer: completer,
onMessage: onMessage!,
);
} else if (execution is ExecuteGentle<R>) {
task = TaskGentle<R>(id: id, workPriority: priority, execution: execution, completer: completer);
} else if (execution is ExecuteGentleWithPort<R>) {
task = TaskGentleWithPort<R>(
id: id,
workPriority: priority,
execution: execution,
completer: completer,
onMessage: onMessage!,
);
}
_queue.add(task);
_schedule();
logTaskAdded(task.id);
return Cancelable(completer: task.completer, onCancel: () => _cancel(task));
}
Future<void> _ensureWorkersInitialized() async {
if (_pool.isEmpty) {
_createWorkers();
if (!_dynamicSpawning) {
await _initializeWorkers();
final poolSize = _pool.length;
final queueSize = _queue.length;
for (int i = 0; i <= min(poolSize, queueSize); i++) {
_schedule();
}
}
}
if (_pool.every((worker) => worker.taskId != null)) {
return;
}
if (_dynamicSpawning) {
final freeWorker = _pool.firstWhereOrNull(
(worker) => worker.taskId == null && !worker.initialized && !worker.initializing,
);
await freeWorker?.initialize();
_schedule();
}
}
void _schedule() {
final availableWorker = _pool.firstWhereOrNull((worker) => worker.taskId == null && worker.initialized);
if (availableWorker == null) {
_ensureWorkersInitialized();
return;
}
if (_queue.isEmpty) return;
final task = _queue.removeFirst();
availableWorker
.work(task)
.then(
(value) {
//could be completed already by cancel and it is normal.
//Assuming that worker finished with error and cleaned gracefully
task.complete(value, null, null);
},
onError: (error, st) {
task.complete(null, error, st);
},
)
.whenComplete(() {
if (_dynamicSpawning && _queue.isEmpty) availableWorker.kill();
_schedule();
});
}
@override
void _cancel(Task task) {
task.cancel();
_queue.remove(task);
final targetWorker = _pool.firstWhereOrNull((worker) => worker.taskId == task.id);
if (task is Gentle) {
targetWorker?.cancelGentle();
} else {
targetWorker?.kill();
if (!_dynamicSpawning) targetWorker?.initialize();
}
super._cancel(task);
}
}

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.144.1
- API version: 2.0.0
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.144.1+3019
version: 2.0.0+3020
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -9858,7 +9858,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.144.1",
"version": "2.0.0",
"contact": {}
},
"tags": [],

View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.144.1",
"version": "2.0.0",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",
@@ -28,6 +28,6 @@
"directory": "open-api/typescript-sdk"
},
"volta": {
"node": "22.19.0"
"node": "22.20.0"
}
}

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.144.1
* 2.0.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -1,12 +1,13 @@
#!/usr/bin/env bash
echo "Initializing Immich $IMMICH_SOURCE_REF"
lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.3"
if [ -f "$lib_path" ]; then
export LD_PRELOAD="$lib_path"
else
echo "skipping libmimalloc - path not found $lib_path"
fi
# TODO: Update to mimalloc v3 when verified memory isn't released issue is fixed
# lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.3"
# if [ -f "$lib_path" ]; then
# export LD_PRELOAD="$lib_path"
# else
# echo "skipping libmimalloc - path not found $lib_path"
# fi
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib"
SERVER_HOME="$(readlink -f "$(dirname "$0")/..")"

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.144.1",
"version": "2.0.0",
"description": "",
"author": "",
"private": true,
@@ -161,7 +161,7 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.19.0"
"node": "22.20.0"
},
"overrides": {
"sharp": "^0.34.3"

View File

@@ -29,8 +29,8 @@ export const AlbumUpdateEmail = ({
</Text>
<Text>
New media has been added to <strong>{albumName}</strong>,
<br /> check it out!
New media has been added to <strong>{albumName}</strong>.
<br /> Check it out!
</Text>
</>
);

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { Button, ButtonProps } from '@react-email/components';
import { Button, ButtonProps, Text } from '@react-email/components';
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"
className="border bg-immich-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 font-bold uppercase"
>
{children}
<Text className="my-3 mx-8">{children}</Text>
</Button>
);

View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.144.1",
"version": "2.0.0",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
@@ -107,6 +107,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.19.0"
"node": "22.20.0"
}
}