Compare commits

..

1 Commits

Author SHA1 Message Date
shenlong-tanwen 1ee679f832 fix: re-enable stale asset pruning 2026-06-23 14:54:46 +05:30
21 changed files with 140 additions and 83 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:a75c5a35bc21d7afe69551eefa3cb1e1fb1775fe759408007a66b54ec3de1f29
image: prom/prometheus@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
-1
View File
@@ -5,7 +5,6 @@ const Map<String, Locale> locales = {
'English (en)': Locale('en'),
// Additional locales
'Arabic (ar)': Locale('ar'),
'Basque (eu)': Locale('eu'),
'Bosnian (bl)': Locale('bn'),
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
'Bulgarian (bg)': Locale('bg'),
@@ -280,8 +280,7 @@ class SyncStreamService {
return;
// SyncCompleteV1 is used to signal the completion of the sync process. Cleanup stale assets and signal completion
case SyncEntityType.syncCompleteV1:
return;
// return _syncStreamRepository.pruneAssets();
return _syncStreamRepository.pruneAssets();
// Request to reset the client state. Clear everything related to remote entities
case SyncEntityType.syncResetV1:
return _syncStreamRepository.reset();
@@ -31,7 +31,7 @@ class AssetBulkUploadCheckResult {
///
Optional<String?> assetId;
/// Client-side identifier echoed from the request to match results to inputs
/// Asset ID
String id;
/// Whether existing asset is trashed
+34 -34
View File
@@ -69,10 +69,10 @@ packages:
dependency: "direct main"
description:
name: background_downloader
sha256: aceacec2b2a72ec3a8862ab5895fcbbc71ab33765f3619d57963f3110dd268e3
sha256: "4cb23d9ad4f5060944f38164e7b90d4bf99b57b2472a3bd4676e59b2db4afd06"
url: "https://pub.dev"
source: hosted
version: "9.5.5"
version: "9.5.4"
bonsoir:
dependency: "direct overridden"
description:
@@ -229,10 +229,10 @@ packages:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.0.0"
code_builder:
dependency: transitive
description:
@@ -326,18 +326,18 @@ packages:
dependency: transitive
description:
name: dbus
sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
url: "https://pub.dev"
source: hosted
version: "0.7.13"
version: "0.7.12"
desktop_webview_window:
dependency: transitive
description:
name: desktop_webview_window
sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957
sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.2.3"
device_info_plus:
dependency: "direct main"
description:
@@ -549,18 +549,18 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff"
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
url: "https://pub.dev"
source: hosted
version: "2.4.8"
version: "2.4.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
version: "2.0.34"
flutter_riverpod:
dependency: transitive
description:
@@ -642,10 +642,10 @@ packages:
dependency: "direct main"
description:
name: flutter_web_auth_2
sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc"
sha256: d354998934ddc338e69b999b2abaeb33c6fd09999d3a5f92ead1a6b49b49712e
url: "https://pub.dev"
source: hosted
version: "5.0.3"
version: "5.0.2"
flutter_web_auth_2_platform_interface:
dependency: transitive
description:
@@ -780,10 +780,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "1.0.3"
hooks_riverpod:
dependency: "direct main"
description:
@@ -844,10 +844,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.9.1"
version: "4.8.0"
image_picker:
dependency: "direct main"
description:
@@ -860,10 +860,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev"
source: hosted
version: "0.8.13+19"
version: "0.8.13+17"
image_picker_for_web:
dependency: transitive
description:
@@ -1120,10 +1120,10 @@ packages:
dependency: transitive
description:
name: native_toolchain_c
sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.19.1"
version: "0.17.6"
native_video_player:
dependency: "direct main"
description:
@@ -1161,10 +1161,10 @@ packages:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
version: "9.3.0"
octo_image:
dependency: "direct main"
description:
@@ -1297,10 +1297,10 @@ packages:
dependency: transitive
description:
name: permission_handler_apple
sha256: e20daf680eef1ca62ffe8c8c526b778cc386d50137c77ac71c8ec9c88c13fb9d
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.9"
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
@@ -1529,10 +1529,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.25"
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
@@ -1791,10 +1791,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
url: "https://pub.dev"
source: hosted
version: "6.3.32"
version: "6.3.30"
url_launcher_ios:
dependency: transitive
description:
@@ -1871,10 +1871,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "7ee12e6dffe0fc8e755179d6d91b3b34f5924223fc104d85572ef9180d73d172"
sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e
url: "https://pub.dev"
source: hosted
version: "1.2.5"
version: "1.2.3"
vector_math:
dependency: transitive
description:
@@ -1991,10 +1991,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "6.6.1"
xxh3:
dependency: transitive
description:
@@ -0,0 +1,64 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import '../repository_context.dart';
void main() {
late MediumRepositoryContext ctx;
late SyncStreamRepository sut;
setUp(() {
ctx = MediumRepositoryContext();
sut = SyncStreamRepository(ctx.db);
});
tearDown(() async {
await ctx.dispose();
});
group('pruneAssets', () {
test('deletes foreign orphans and keeps owned, partner, and in-album assets', () async {
final me = await ctx.newUser();
final partner = await ctx.newUser();
final stranger = await ctx.newUser();
await ctx.newAuthUser(id: me.id);
await ctx.newPartner(sharedById: partner.id, sharedWithId: me.id);
final own = await ctx.newRemoteAsset(ownerId: me.id);
final fromPartner = await ctx.newRemoteAsset(ownerId: partner.id);
final shared = await ctx.newRemoteAsset(ownerId: stranger.id);
await ctx.newRemoteAsset(ownerId: stranger.id);
final album = await ctx.newRemoteAlbum(ownerId: me.id);
await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: shared.id);
await sut.pruneAssets();
final remaining = await ctx.db.select(ctx.db.remoteAssetEntity).get();
expect(remaining.map((a) => a.id), unorderedEquals([own.id, fromPartner.id, shared.id]));
});
test('does nothing when there is no authenticated user', () async {
final stranger = await ctx.newUser();
final orphan = await ctx.newRemoteAsset(ownerId: stranger.id);
await sut.pruneAssets();
final remaining = await ctx.db.select(ctx.db.remoteAssetEntity).get();
expect(remaining.map((a) => a.id), [orphan.id]);
});
test('prunes every stale foreign asset in a large data set', () async {
final stranger = await ctx.newUser();
await ctx.newAuthUser();
for (var i = 0; i < 600; i++) {
await ctx.newRemoteAsset(ownerId: stranger.id);
}
await sut.pruneAssets();
final remaining = await ctx.db.select(ctx.db.remoteAssetEntity).get();
expect(remaining, isEmpty);
});
});
}
@@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
@@ -72,6 +73,20 @@ class MediumRepositoryContext {
);
}
Future<AuthUserEntityData> newAuthUser({String? id, String? email, AvatarColor? avatarColor}) async {
id ??= TestUtils.uuid();
return await db
.into(db.authUserEntity)
.insertReturning(
AuthUserEntityCompanion(
id: .new(id),
email: .new(email ?? '$id@test.com'),
name: .new('user_$id'),
avatarColor: .new(avatarColor ?? TestUtils.randElement(AvatarColor.values)),
),
);
}
Future<void> newPartner({required String sharedById, required String sharedWithId, bool? inTimeline}) {
return db
.into(db.partnerEntity)
+3 -3
View File
@@ -17012,12 +17012,12 @@
},
"assetId": {
"description": "Existing asset ID if duplicate",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
},
"id": {
"description": "Client-side identifier echoed from the request to match results to inputs",
"description": "Asset ID",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
},
"isTrashed": {
+3 -11
View File
@@ -278,9 +278,7 @@
"title": "Album IDs",
"array": true,
"description": "Target album IDs",
"uiHint": {
"type": "AlbumId"
}
"uiHint": "AlbumId"
},
"albumName": {
"type": "string",
@@ -370,20 +368,14 @@
"type": "string",
"title": "Album ID",
"description": "Target album ID",
"uiHint": {
"type": "AlbumId",
"order": 1
}
"uiHint": "AlbumId"
},
"albumIds": {
"type": "string",
"title": "Album IDs",
"description": "Target album IDs",
"array": true,
"uiHint": {
"type": "AlbumId",
"order": 2
}
"uiHint": "AlbumId"
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@
"@types/node": "^24.13.2",
"esbuild": "^0.28.0",
"tsc-alias": "^1.8.16",
"typescript": "^6.0.0"
"typescript": "^5.9.3"
},
"peerDependencies": {
"@extism/js-pdk": "^1.1.1"
+1 -1
View File
@@ -8,7 +8,7 @@ import type {
} from 'src/types.js';
export const wrapper = <
T extends WorkflowType,
T extends WorkflowType = WorkflowType,
TConfig extends ConfigValue = ConfigValue,
>(
fn: (
+5 -6
View File
@@ -11,7 +11,7 @@ type DeepPartial<T> = T extends Date
export type WorkflowEventMap = {
[WorkflowType.AssetV1]: AssetV1;
// [WorkflowType.AssetPersonV1]: AssetPersonV1;
} & { [K in WorkflowType]: unknown };
};
export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
@@ -22,7 +22,7 @@ export enum WorkflowTrigger {
}
export type WorkflowEventPayload<
T extends WorkflowType,
T extends WorkflowType = WorkflowType,
TConfig = WorkflowStepConfig,
> = {
trigger: WorkflowTrigger;
@@ -37,11 +37,10 @@ export type WorkflowEventPayload<
};
};
export type WorkflowChanges<T extends WorkflowType> = DeepPartial<
WorkflowEventData<T>
>;
export type WorkflowChanges<T extends WorkflowType = WorkflowType> =
DeepPartial<WorkflowEventData<T>>;
export type WorkflowResponse<T extends WorkflowType> = {
export type WorkflowResponse<T extends WorkflowType = WorkflowType> = {
workflow?: {
/** stop the workflow */
continue?: boolean;
-1
View File
@@ -20,7 +20,6 @@
"sourceMap": false,
"strict": true,
"target": "esnext",
"typeRoots": ["./node_modules/@types", "./node_modules"],
"types": ["node", "@extism/js-pdk"],
"verbatimModuleSyntax": true
}
+1 -1
View File
@@ -707,7 +707,7 @@ export type AssetBulkUploadCheckResult = {
action: AssetUploadAction;
/** Existing asset ID if duplicate */
assetId?: string;
/** Client-side identifier echoed from the request to match results to inputs */
/** Asset ID */
id: string;
/** Whether existing asset is trashed */
isTrashed?: boolean;
+2 -2
View File
@@ -353,8 +353,8 @@ importers:
specifier: ^1.8.16
version: 1.8.17
typescript:
specifier: ^6.0.0
version: 6.0.3
specifier: ^5.9.3
version: 5.9.3
packages/sdk:
dependencies:
+2 -2
View File
@@ -34,10 +34,10 @@ const AssetRejectReasonSchema = z
const AssetBulkUploadCheckResultSchema = z
.object({
id: z.string().describe('Client-side identifier echoed from the request to match results to inputs'),
id: z.uuidv4().describe('Asset ID'),
action: AssetUploadActionSchema,
reason: AssetRejectReasonSchema.optional(),
assetId: z.uuidv4().optional().describe('Existing asset ID if duplicate'),
assetId: z.string().optional().describe('Existing asset ID if duplicate'),
isTrashed: z.boolean().optional().describe('Whether existing asset is trashed'),
})
.meta({ id: 'AssetBulkUploadCheckResult' });
+1 -6
View File
@@ -14,12 +14,7 @@ const JsonSchemaPropertySchema = z
enum: z.array(z.string()).optional().describe('Valid choices for enum types'),
array: z.boolean().optional().describe('Type is an array type'),
required: z.array(z.string()).optional().describe('A list of required properties'),
uiHint: z
.object({
type: z.string().optional(),
order: z.int().optional(),
})
.optional(),
uiHint: z.string().optional(),
get properties() {
return z.record(z.string(), JsonSchemaPropertySchema).optional();
},
@@ -369,7 +369,7 @@ export class WorkflowExecutionService extends BaseService {
const readResult = await read(type);
let data = readResult.data;
for (const step of workflow.steps) {
const payload: WorkflowEventPayload<typeof type> = {
const payload: WorkflowEventPayload = {
trigger: workflow.trigger,
type,
config: step.config ?? {},
@@ -51,8 +51,6 @@
};
const setUiHintValue = (values: string[]) => setValue(schema.array ? values : values[0]);
const getSchemaProperties = (schema: JSONSchemaProperty) =>
Object.entries(schema.properties ?? {}).sort((a, b) => (a[1].uiHint?.order ?? 0) - (b[1].uiHint?.order ?? 0));
const getBoolean = (defaultValue = false) => getValue<boolean>(defaultValue);
const getString = () => getValue<string>();
@@ -74,11 +72,11 @@
</div>
{/if}
<div class="flex flex-col gap-4 {root ? '' : 'border-l-4 border-gray-200 ps-2'}">
{#each getSchemaProperties(schema) as [childKey, childSchema] (childKey)}
{#each Object.entries(schema.properties ?? {}) as [childKey, childSchema] (childKey)}
<Self schema={childSchema} key={childKey} bind:config={getValue, setValue} />
{/each}
</div>
{:else if schema.uiHint?.type === 'AlbumId'}
{:else if schema.uiHint === 'AlbumId'}
<SchemaAlbumPicker {label} {description} array={schema.array} bind:albumIds={getUiHintValue, setUiHintValue} />
{:else if schema.enum && schema.array}
<Field {label} {description}>
+1 -4
View File
@@ -96,10 +96,7 @@ export type JSONSchemaProperty = {
array?: boolean;
properties?: Record<string, JSONSchemaProperty>;
required?: string[];
uiHint?: {
type?: 'AlbumId' | 'AssetId' | 'PersonId';
order?: number;
};
uiHint?: 'AlbumId' | 'AssetId' | 'PersonId';
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -55,7 +55,7 @@
);
const isGhost = $derived(step.id === 'ghost');
const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint?.type;
const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint;
const toIds = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : [String(value)]);
let dragImage = $state<Element>();
let isDropTarget = $state(false);