From 3eb2bf0342387f30d11d1216a749a47e676b7fc8 Mon Sep 17 00:00:00 2001
From: Peter Ombodi
Date: Mon, 6 Oct 2025 11:41:34 +0300
Subject: [PATCH] optimize, refactor code remove redundant code and checking
getTrashedAssetsForAlbum for iOS tests for hash trashed assets
---
.../app/alextran/immich/sync/Messages.g.kt | 74 +------
.../alextran/immich/sync/MessagesImpl26.kt | 7 -
.../alextran/immich/sync/MessagesImpl30.kt | 55 -----
.../alextran/immich/sync/MessagesImplBase.kt | 8 -
mobile/ios/Podfile.lock | 62 +++---
mobile/ios/Runner/Sync/Messages.g.swift | 68 +------
mobile/ios/Runner/Sync/MessagesImpl.swift | 123 ++++++------
.../models/asset/trashed_asset.model.dart | 7 -
mobile/lib/domain/services/hash.service.dart | 107 ++--------
.../domain/services/local_sync.service.dart | 101 ++++++++--
.../domain/services/sync_stream.service.dart | 2 +-
.../domain/services/trash_sync.service.dart | 91 +--------
.../entities/trashed_local_asset.entity.dart | 3 -
.../trashed_local_asset.entity.drift.dart | 71 +------
.../repositories/local_asset.repository.dart | 25 +--
.../trashed_local_asset.repository.dart | 189 +++++++++---------
mobile/lib/platform/native_sync_api.g.dart | 95 +--------
.../infrastructure/sync.provider.dart | 6 +-
.../infrastructure/trash_sync.provider.dart | 7 +-
.../sync_status_and_actions.dart | 2 +-
mobile/pigeon/native_sync_api.dart | 24 +--
.../domain/services/hash_service_test.dart | 42 +++-
mobile/test/fixtures/asset.stub.dart | 14 ++
.../test/infrastructure/repository.mock.dart | 3 +
24 files changed, 393 insertions(+), 793 deletions(-)
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt
index 7bba2508f9..fc3cb2b3a9 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt
@@ -89,9 +89,7 @@ data class PlatformAsset (
val height: Long? = null,
val durationInSeconds: Long,
val orientation: Long,
- val isFavorite: Boolean,
- val isTrashed: Boolean? = null,
- val volume: String? = null
+ val isFavorite: Boolean
)
{
companion object {
@@ -106,9 +104,7 @@ data class PlatformAsset (
val durationInSeconds = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long
val isFavorite = pigeonVar_list[9] as Boolean
- val isTrashed = pigeonVar_list[10] as Boolean?
- val volume = pigeonVar_list[11] as String?
- return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, isTrashed, volume)
+ return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite)
}
}
fun toList(): List {
@@ -123,8 +119,6 @@ data class PlatformAsset (
durationInSeconds,
orientation,
isFavorite,
- isTrashed,
- volume,
)
}
override fun equals(other: Any?): Boolean {
@@ -249,40 +243,6 @@ data class HashResult (
override fun hashCode(): Int = toList().hashCode()
}
-
-/** Generated class from Pigeon that represents data sent in messages. */
-data class TrashedAssetParams (
- val id: String,
- val type: Long,
- val albumId: String? = null
-)
- {
- companion object {
- fun fromList(pigeonVar_list: List): TrashedAssetParams {
- val id = pigeonVar_list[0] as String
- val type = pigeonVar_list[1] as Long
- val albumId = pigeonVar_list[2] as String?
- return TrashedAssetParams(id, type, albumId)
- }
- }
- fun toList(): List {
- return listOf(
- id,
- type,
- albumId,
- )
- }
- override fun equals(other: Any?): Boolean {
- if (other !is TrashedAssetParams) {
- return false
- }
- if (this === other) {
- return true
- }
- return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
-
- override fun hashCode(): Int = toList().hashCode()
-}
private open class MessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -306,11 +266,6 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
HashResult.fromList(it)
}
}
- 133.toByte() -> {
- return (readValue(buffer) as? List)?.let {
- TrashedAssetParams.fromList(it)
- }
- }
else -> super.readValueOfType(type, buffer)
}
}
@@ -332,10 +287,6 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
stream.write(132)
writeValue(stream, value.toList())
}
- is TrashedAssetParams -> {
- stream.write(133)
- writeValue(stream, value.toList())
- }
else -> super.writeValue(stream, value)
}
}
@@ -355,7 +306,6 @@ interface NativeSyncApi {
fun hashAssets(assetIds: List, allowNetworkAccess: Boolean, callback: (Result>) -> Unit)
fun cancelHashing()
fun getTrashedAssetsForAlbum(albumId: String): List
- fun hashTrashedAssets(trashedAssets: List, callback: (Result>) -> Unit)
companion object {
/** The codec used by NativeSyncApi. */
@@ -553,26 +503,6 @@ interface NativeSyncApi {
channel.setMessageHandler(null)
}
}
- run {
- val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
- if (api != null) {
- channel.setMessageHandler { message, reply ->
- val args = message as List
- val trashedAssetsArg = args[0] as List
- api.hashTrashedAssets(trashedAssetsArg) { result: Result> ->
- val error = result.exceptionOrNull()
- if (error != null) {
- reply.reply(MessagesPigeonUtils.wrapError(error))
- } else {
- val data = result.getOrNull()
- reply.reply(MessagesPigeonUtils.wrapResult(data))
- }
- }
- }
- } else {
- channel.setMessageHandler(null)
- }
- }
}
}
}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt
index 767ef8e026..1f42a55706 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt
@@ -27,11 +27,4 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
): List {
throw IllegalStateException("Method not supported on this Android version.")
}
-
- override fun hashTrashedAssets(
- trashedAssets: List,
- callback: (Result>) -> Unit
- ) {
- throw IllegalStateException("Method not supported on this Android version.")
- }
}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt
index 8f9052cf90..b43bf7ab72 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt
@@ -130,59 +130,4 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
return trashed
}
-
- override fun hashTrashedAssets(
- trashedAssets: List,
- callback: (Result>) -> Unit
- ) {
- if (trashedAssets.isEmpty()) {
- callback(Result.success(emptyList()))
- return
- }
- hashTask?.cancel()
- hashTask = CoroutineScope(Dispatchers.IO).launch {
- try {
- val results = trashedAssets.map { assetParams ->
- async {
- hashSemaphore.withPermit {
- ensureActive()
- hashTrashedAsset(assetParams)
- }
- }
- }.awaitAll()
-
- callback(Result.success(results))
- } catch (e: CancellationException) {
- callback(
- Result.failure(
- FlutterError(
- HASHING_CANCELLED_CODE,
- "Hashing operation was cancelled",
- null
- )
- )
- )
- } catch (e: Exception) {
- callback(Result.failure(e))
- }
- }
- }
-
- suspend fun hashTrashedAsset(assetParams: TrashedAssetParams): HashResult {
- val id = assetParams.id.toLong()
- val mediaType = assetParams.type.toInt()
- val assetUri = contentUriForType(id, mediaType)
- return hashAssetFromUri(assetParams.id, assetUri)
- }
-
- private fun contentUriForType(id: Long, mediaType: Int): Uri {
- val vol = MediaStore.VOLUME_EXTERNAL
- val base = when (mediaType) {
- MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> MediaStore.Images.Media.getContentUri(vol)
- MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.getContentUri(vol)
- MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO -> MediaStore.Audio.Media.getContentUri(vol)
- else -> MediaStore.Files.getContentUri(vol)
- }
- return ContentUris.withAppendedId(base, id)
- }
}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt
index 5ed62c8930..d1882dc768 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt
@@ -62,8 +62,6 @@ open class NativeSyncApiImplBase(context: Context) {
// only available on Android 11 and above
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
add(MediaStore.MediaColumns.IS_FAVORITE)
- add(MediaStore.MediaColumns.IS_TRASHED)
- add(MediaStore.MediaColumns.VOLUME_NAME)
}
}.toTypedArray()
@@ -111,8 +109,6 @@ open class NativeSyncApiImplBase(context: Context) {
val orientationColumn =
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
- val trashedColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_TRASHED)
- val volumeColumn = c.getColumnIndex(MediaStore.MediaColumns.VOLUME_NAME)
while (c.moveToNext()) {
val id = c.getLong(idColumn).toString()
@@ -142,8 +138,6 @@ open class NativeSyncApiImplBase(context: Context) {
val bucketId = c.getString(bucketIdColumn)
val orientation = c.getInt(orientationColumn)
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
- val isTrashed = if (trashedColumn == -1) null else c.getInt(trashedColumn) != 0
- val volume = if (volumeColumn == -1) null else c.getString(volumeColumn)
val asset = PlatformAsset(
id,
name,
@@ -155,8 +149,6 @@ open class NativeSyncApiImplBase(context: Context) {
duration,
orientation.toLong(),
isFavorite,
- isTrashed,
- volume,
)
yield(AssetResult.ValidAsset(asset, bucketId))
}
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 502fd9008f..13f081d66b 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -246,46 +246,46 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
- background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
- bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
- connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
- device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
+ background_downloader: a05c77d32a0d70615b9c04577aa203535fc924ff
+ bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
+ connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
+ device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
- file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
+ file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
- flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
- flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
- flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
- flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
- flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
- fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
- geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
- home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
- image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
- integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
- isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
- local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
+ flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
+ flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
+ flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
+ flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
+ flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
+ fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
+ geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
+ home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
+ image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
+ integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
+ isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
+ local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
- maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
- native_video_player: b65c58951ede2f93d103a25366bdebca95081265
- network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
- package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
- path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
- permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
- photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
+ maplibre_gl: 753f55d763a81cbdba087d02af02d12206e6f94e
+ native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
+ network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
+ package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
+ path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
+ permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
+ photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
- share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
+ share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
- share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
- shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
- sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
+ share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
+ shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
+ sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
- sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
+ sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
- url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
- wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
+ url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
+ wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift
index 8306214110..ea0ba53bef 100644
--- a/mobile/ios/Runner/Sync/Messages.g.swift
+++ b/mobile/ios/Runner/Sync/Messages.g.swift
@@ -140,8 +140,6 @@ struct PlatformAsset: Hashable {
var durationInSeconds: Int64
var orientation: Int64
var isFavorite: Bool
- var isTrashed: Bool? = nil
- var volume: String? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
@@ -156,8 +154,6 @@ struct PlatformAsset: Hashable {
let durationInSeconds = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64
let isFavorite = pigeonVar_list[9] as! Bool
- let isTrashed: Bool? = nilOrValue(pigeonVar_list[10])
- let volume: String? = nilOrValue(pigeonVar_list[11])
return PlatformAsset(
id: id,
@@ -169,9 +165,7 @@ struct PlatformAsset: Hashable {
height: height,
durationInSeconds: durationInSeconds,
orientation: orientation,
- isFavorite: isFavorite,
- isTrashed: isTrashed,
- volume: volume
+ isFavorite: isFavorite
)
}
func toList() -> [Any?] {
@@ -186,8 +180,6 @@ struct PlatformAsset: Hashable {
durationInSeconds,
orientation,
isFavorite,
- isTrashed,
- volume,
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
@@ -308,39 +300,6 @@ struct HashResult: Hashable {
}
}
-/// Generated class from Pigeon that represents data sent in messages.
-struct TrashedAssetParams: Hashable {
- var id: String
- var type: Int64
- var albumId: String? = nil
-
-
- // swift-format-ignore: AlwaysUseLowerCamelCase
- static func fromList(_ pigeonVar_list: [Any?]) -> TrashedAssetParams? {
- let id = pigeonVar_list[0] as! String
- let type = pigeonVar_list[1] as! Int64
- let albumId: String? = nilOrValue(pigeonVar_list[2])
-
- return TrashedAssetParams(
- id: id,
- type: type,
- albumId: albumId
- )
- }
- func toList() -> [Any?] {
- return [
- id,
- type,
- albumId,
- ]
- }
- static func == (lhs: TrashedAssetParams, rhs: TrashedAssetParams) -> Bool {
- return deepEqualsMessages(lhs.toList(), rhs.toList()) }
- func hash(into hasher: inout Hasher) {
- deepHashMessages(value: toList(), hasher: &hasher)
- }
-}
-
private class MessagesPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
@@ -352,8 +311,6 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
return SyncDelta.fromList(self.readValue() as! [Any?])
case 132:
return HashResult.fromList(self.readValue() as! [Any?])
- case 133:
- return TrashedAssetParams.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
@@ -374,9 +331,6 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter {
} else if let value = value as? HashResult {
super.writeByte(132)
super.writeValue(value.toList())
- } else if let value = value as? TrashedAssetParams {
- super.writeByte(133)
- super.writeValue(value.toList())
} else {
super.writeValue(value)
}
@@ -411,7 +365,6 @@ protocol NativeSyncApi {
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
func cancelHashing() throws
func getTrashedAssetsForAlbum(albumId: String) throws -> [PlatformAsset]
- func hashTrashedAssets(trashedAssets: [TrashedAssetParams], completion: @escaping (Result<[HashResult], Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -599,24 +552,5 @@ class NativeSyncApiSetup {
} else {
getTrashedAssetsForAlbumChannel.setMessageHandler(nil)
}
- let hashTrashedAssetsChannel = taskQueue == nil
- ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
- : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
- if let api = api {
- hashTrashedAssetsChannel.setMessageHandler { message, reply in
- let args = message as! [Any?]
- let trashedAssetsArg = args[0] as! [TrashedAssetParams]
- api.hashTrashedAssets(trashedAssets: trashedAssetsArg) { result in
- switch result {
- case .success(let res):
- reply(wrapResult(res))
- case .failure(let error):
- reply(wrapError(error))
- }
- }
- }
- } else {
- hashTrashedAssetsChannel.setMessageHandler(nil)
- }
}
}
diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift
index bb23bae6b6..38849124b5 100644
--- a/mobile/ios/Runner/Sync/MessagesImpl.swift
+++ b/mobile/ios/Runner/Sync/MessagesImpl.swift
@@ -3,15 +3,15 @@ import CryptoKit
struct AssetWrapper: Hashable, Equatable {
let asset: PlatformAsset
-
+
init(with asset: PlatformAsset) {
self.asset = asset
}
-
+
func hash(into hasher: inout Hasher) {
hasher.combine(self.asset.id)
}
-
+
static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
return lhs.asset.id == rhs.asset.id
}
@@ -22,16 +22,16 @@ class NativeSyncApiImpl: NativeSyncApi {
private let changeTokenKey = "immich:changeToken"
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
private let recoveredAlbumSubType = 1000000219
-
+
private var hashTask: Task?
private static let hashCancelledCode = "HASH_CANCELLED"
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
-
-
+
+
init(with defaults: UserDefaults = .standard) {
self.defaults = defaults
}
-
+
@available(iOS 16, *)
private func getChangeToken() -> PHPersistentChangeToken? {
guard let data = defaults.data(forKey: changeTokenKey) else {
@@ -39,7 +39,7 @@ class NativeSyncApiImpl: NativeSyncApi {
}
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data)
}
-
+
@available(iOS 16, *)
private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else {
@@ -47,18 +47,18 @@ class NativeSyncApiImpl: NativeSyncApi {
}
defaults.set(data, forKey: changeTokenKey)
}
-
+
func clearSyncCheckpoint() -> Void {
defaults.removeObject(forKey: changeTokenKey)
}
-
+
func checkpointSync() {
guard #available(iOS 16, *) else {
return
}
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
}
-
+
func shouldFullSync() -> Bool {
guard #available(iOS 16, *),
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
@@ -66,34 +66,34 @@ class NativeSyncApiImpl: NativeSyncApi {
// When we do not have access to photo library, older iOS version or No token available, fallback to full sync
return true
}
-
+
guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else {
// Cannot fetch persistent changes
return true
}
-
+
return false
}
-
+
func getAlbums() throws -> [PlatformAlbum] {
var albums: [PlatformAlbum] = []
-
+
albumTypes.forEach { type in
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
for i in 0.. SyncDelta {
+
+ func getMediaChanges(isTrashed: Bool) throws -> SyncDelta {
guard #available(iOS 16, *) else {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
}
-
+
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
}
-
+
guard let storedToken = getChangeToken() else {
// No token exists, definitely need a full sync
print("MediaManager::getMediaChanges: No token found")
throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
}
-
+
let currentToken = PHPhotoLibrary.shared().currentChangeToken
if storedToken == currentToken {
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
}
-
+
do {
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
-
+
var updatedAssets: Set = []
var deletedAssets: Set = []
-
+
for change in changes {
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
-
+
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
deletedAssets.formUnion(details.deletedLocalIdentifiers)
-
+
if (updated.isEmpty) { continue }
-
+
let options = PHFetchOptions()
options.includeHiddenAssets = false
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
for i in 0..) -> [String: [String]] {
guard !assets.isEmpty else {
return [:]
}
-
+
var albumAssets: [String: [String]] = [:]
-
+
for type in albumTypes {
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
collections.enumerateObjects { (album, _, _) in
@@ -197,13 +197,13 @@ class NativeSyncApiImpl: NativeSyncApi {
}
return albumAssets
}
-
+
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []
}
-
+
var ids: [String] = []
let options = PHFetchOptions()
options.includeHiddenAssets = false
@@ -213,13 +213,13 @@ class NativeSyncApiImpl: NativeSyncApi {
}
return ids
}
-
+
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return 0
}
-
+
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
@@ -227,32 +227,32 @@ class NativeSyncApiImpl: NativeSyncApi {
let assets = PHAsset.fetchAssets(in: album, options: options)
return Int64(assets.count)
}
-
+
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []
}
-
+
let options = PHFetchOptions()
options.includeHiddenAssets = false
if(updatedTimeCond != nil) {
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
}
-
+
let result = PHAsset.fetchAssets(in: album, options: options)
if(result.count == 0) {
return []
}
-
+
var assets: [PlatformAsset] = []
result.enumerateObjects { (asset, _, _) in
assets.append(asset.toPlatformAsset())
}
return assets
}
-
+
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) {
if let prevTask = hashTask {
prevTask.cancel()
@@ -270,11 +270,11 @@ class NativeSyncApiImpl: NativeSyncApi {
missingAssetIds.remove(asset.localIdentifier)
assets.append(asset)
}
-
+
if Task.isCancelled {
return completion(Self.hashCancelled)
}
-
+
await withTaskGroup(of: HashResult?.self) { taskGroup in
var results = [HashResult]()
results.reserveCapacity(assets.count)
@@ -287,28 +287,28 @@ class NativeSyncApiImpl: NativeSyncApi {
return await self.hashAsset(asset, allowNetworkAccess: allowNetworkAccess)
}
}
-
+
for await result in taskGroup {
guard let result = result else {
return completion(Self.hashCancelled)
}
results.append(result)
}
-
+
for missing in missingAssetIds {
results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil))
}
-
+
completion(.success(results))
}
}
}
-
+
func cancelHashing() {
hashTask?.cancel()
hashTask = nil
}
-
+
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
class RequestRef {
var id: PHAssetResourceDataRequestID?
@@ -318,21 +318,21 @@ class NativeSyncApiImpl: NativeSyncApi {
if Task.isCancelled {
return nil
}
-
+
guard let resource = asset.getResource() else {
return HashResult(assetId: asset.localIdentifier, error: "Cannot get asset resource", hash: nil)
}
-
+
if Task.isCancelled {
return nil
}
-
+
let options = PHAssetResourceRequestOptions()
options.isNetworkAccessAllowed = allowNetworkAccess
-
+
return await withCheckedContinuation { continuation in
var hasher = Insecure.SHA1()
-
+
requestRef.id = PHAssetResourceManager.default().requestData(
for: resource,
options: options,
@@ -363,4 +363,9 @@ class NativeSyncApiImpl: NativeSyncApi {
PHAssetResourceManager.default().cancelDataRequest(requestId)
})
}
+
+ func getTrashedAssetsForAlbum(albumId: String) throws ->[PlatformAsset] {
+throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
+}
+
}
diff --git a/mobile/lib/domain/models/asset/trashed_asset.model.dart b/mobile/lib/domain/models/asset/trashed_asset.model.dart
index 79e507dc06..44732b3b01 100644
--- a/mobile/lib/domain/models/asset/trashed_asset.model.dart
+++ b/mobile/lib/domain/models/asset/trashed_asset.model.dart
@@ -1,12 +1,10 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class TrashedAsset extends LocalAsset {
- final String? volume;
final String albumId;
const TrashedAsset({
required this.albumId,
- this.volume,
required super.id,
super.remoteId,
required super.name,
@@ -38,7 +36,6 @@ class TrashedAsset extends LocalAsset {
String? livePhotoVideoId,
int? orientation,
String? albumId,
- String? volume,
}) {
return TrashedAsset(
id: id ?? this.id,
@@ -55,7 +52,6 @@ class TrashedAsset extends LocalAsset {
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
orientation: orientation ?? this.orientation,
albumId: albumId ?? this.albumId,
- volume: volume ?? this.volume,
);
}
@@ -79,7 +75,6 @@ class TrashedAsset extends LocalAsset {
livePhotoVideoId == other.livePhotoVideoId &&
orientation == other.orientation &&
// TrashedAsset extras
- volume == other.volume &&
albumId == other.albumId;
@override
@@ -97,7 +92,6 @@ class TrashedAsset extends LocalAsset {
isFavorite,
livePhotoVideoId,
orientation,
- volume,
albumId,
);
@@ -118,7 +112,6 @@ class TrashedAsset extends LocalAsset {
'livePhotoVideoId: $livePhotoVideoId, '
'orientation: $orientation, '
'albumId: $albumId, '
- 'volume: $volume'
')';
}
}
diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart
index 4dcddf85d6..de72df1e1b 100644
--- a/mobile/lib/domain/services/hash.service.dart
+++ b/mobile/lib/domain/services/hash.service.dart
@@ -2,10 +2,10 @@ import 'package:flutter/services.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
-import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
-import 'package:immich_mobile/domain/services/trash_sync.service.dart';
+import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
+import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:logging/logging.dart';
@@ -15,24 +15,24 @@ class HashService {
final int _batchSize;
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftLocalAssetRepository _localAssetRepository;
+ final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final NativeSyncApi _nativeSyncApi;
- final TrashSyncService _trashSyncService;
final bool Function()? _cancelChecker;
final _log = Logger('HashService');
HashService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
+ required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required NativeSyncApi nativeSyncApi,
- required TrashSyncService trashSyncService,
bool Function()? cancelChecker,
int? batchSize,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
+ _trashedLocalAssetRepository = trashedLocalAssetRepository,
_cancelChecker = cancelChecker,
_nativeSyncApi = nativeSyncApi,
- _batchSize = batchSize ?? kBatchHashFileLimit,
- _trashSyncService = trashSyncService;
+ _batchSize = batchSize ?? kBatchHashFileLimit;
bool get isCancelled => _cancelChecker?.call() ?? false;
@@ -54,6 +54,14 @@ class HashService {
await _hashAssets(album, assetsToHash);
}
}
+ if (CurrentPlatform.isAndroid && localAlbums.isNotEmpty) {
+ final backupAlbumIds = localAlbums.map((e) => e.id);
+ final trashedToHash = await _trashedLocalAssetRepository.getAssetsToHash(backupAlbumIds);
+ if (trashedToHash.isNotEmpty) {
+ final pseudoAlbum = LocalAlbum(id: '-pseudoAlbum', name: 'Trash', updatedAt: DateTime.now());
+ await _hashAssets(pseudoAlbum, trashedToHash.toList(), isTrashed: true);
+ }
+ }
} on PlatformException catch (e) {
if (e.code == _kHashCancelledCode) {
_log.warning("Hashing cancelled by platform");
@@ -63,23 +71,6 @@ class HashService {
_log.severe("Error during hashing", e, s);
}
- if (_trashSyncService.isAutoSyncMode) {
- final backupAlbums = await _localAlbumRepository.getBackupAlbums();
- if (backupAlbums.isNotEmpty) {
- final backupAlbumIds = backupAlbums.map((e) => e.id);
- final trashedToHash = await _trashSyncService.getAssetsToHash(backupAlbumIds);
- if (trashedToHash.isNotEmpty) {
- for (final album in backupAlbums) {
- if (isCancelled) {
- _log.warning("Hashing cancelled. Stopped processing albums.");
- break;
- }
- await _hashTrashedAssets(album, trashedToHash.where((e) => e.albumId == album.id));
- }
- }
- }
- }
-
stopwatch.stop();
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
}
@@ -87,7 +78,7 @@ class HashService {
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
/// with hash for those that were successfully hashed. Hashes are looked up in a table
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
- Future _hashAssets(LocalAlbum album, List assetsToHash) async {
+ Future _hashAssets(LocalAlbum album, List assetsToHash, {bool isTrashed = false}) async {
final toHash = {};
for (final asset in assetsToHash) {
@@ -98,16 +89,16 @@ class HashService {
toHash[asset.id] = asset;
if (toHash.length == _batchSize) {
- await _processBatch(album, toHash);
+ await _processBatch(album, toHash, isTrashed);
toHash.clear();
}
}
- await _processBatch(album, toHash);
+ await _processBatch(album, toHash, isTrashed);
}
/// Processes a batch of assets.
- Future _processBatch(LocalAlbum album, Map toHash) async {
+ Future _processBatch(LocalAlbum album, Map toHash, bool isTrashed) async {
if (toHash.isEmpty) {
return;
}
@@ -142,64 +133,10 @@ class HashService {
}
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
-
- await _localAssetRepository.updateHashes(hashed);
- }
-
- Future _hashTrashedAssets(LocalAlbum album, Iterable assetsToHash) async {
- final toHash = [];
-
- for (final asset in assetsToHash) {
- if (isCancelled) {
- _log.warning("Hashing cancelled. Stopped processing assets.");
- return;
- }
-
- toHash.add(asset);
-
- if (toHash.length == _batchSize) {
- await _processTrashedBatch(album, toHash);
- toHash.clear();
- }
+ if (isTrashed) {
+ await _trashedLocalAssetRepository.updateHashes(hashed);
+ } else {
+ await _localAssetRepository.updateHashes(hashed);
}
-
- await _processTrashedBatch(album, toHash);
- }
-
- Future _processTrashedBatch(LocalAlbum album, List toHash) async {
- if (toHash.isEmpty) {
- return;
- }
-
- _log.fine("Hashing ${toHash.length} trashed files");
-
- final params = toHash.map((e) => TrashedAssetParams(id: e.id, type: e.type.index, albumId: album.id)).toList();
- final hashResults = await _nativeSyncApi.hashTrashedAssets(params);
-
- assert(
- hashResults.length == toHash.length,
- "Trashed Assets, Hashes length does not match toHash length: ${hashResults.length} != ${toHash.length}",
- );
- final hashed = [];
-
- for (int i = 0; i < hashResults.length; i++) {
- if (isCancelled) {
- _log.warning("Hashing cancelled. Stopped processing batch.");
- return;
- }
-
- final hashResult = hashResults[i];
- final asset = toHash[i];
- if (hashResult.hash != null) {
- hashed.add(asset.copyWith(checksum: hashResult.hash!));
- } else {
- _log.warning(
- "Failed to hash trashed asset with id: ${hashResult.assetId}, name: ${asset.name}, createdAt: ${asset.createdAt}, from album: ${album.name}. Error: ${hashResult.error ?? "unknown"}",
- );
- }
- }
-
- _log.fine("Hashed ${hashed.length}/${toHash.length} trashed assets");
- await _trashSyncService.updateChecksums(hashed);
}
}
diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart
index 12c996f472..b522fa423f 100644
--- a/mobile/lib/domain/services/local_sync.service.dart
+++ b/mobile/lib/domain/services/local_sync.service.dart
@@ -4,10 +4,12 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
-import 'package:immich_mobile/domain/services/trash_sync.service.dart';
+import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
+import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
+import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
@@ -15,15 +17,18 @@ import 'package:logging/logging.dart';
class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository;
final NativeSyncApi _nativeSyncApi;
- final TrashSyncService _trashSyncService;
+ final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
+ final LocalFilesManagerRepository _localFilesManager;
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository,
- required TrashSyncService trashSyncService,
+ required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
+ required LocalFilesManagerRepository localFilesManager,
required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository,
- _trashSyncService = trashSyncService,
+ _trashedLocalAssetRepository = trashedLocalAssetRepository,
+ _localFilesManager = localFilesManager,
_nativeSyncApi = nativeSyncApi;
Future sync({bool full = false}) async {
@@ -34,6 +39,12 @@ class LocalSyncService {
return await fullSync();
}
+ if (CurrentPlatform.isAndroid) {
+ final delta = await _nativeSyncApi.getMediaChanges(isTrashed: true);
+ _log.fine("Delta updated in trash: ${delta.updates.length - delta.updates.length}");
+ await _applyTrashDelta(delta);
+ }
+
final delta = await _nativeSyncApi.getMediaChanges();
if (!delta.hasChanges) {
_log.fine("No media changes detected. Skipping sync");
@@ -75,11 +86,6 @@ class LocalSyncService {
await updateAlbum(dbAlbum, album);
}
}
- if (_trashSyncService.isAutoSyncMode) {
- final delta = await _nativeSyncApi.getMediaChanges(isTrashed: true);
- _log.fine("Delta updated in trash: ${delta.updates.length - delta.updates.length}");
- await _trashSyncService.applyTrashDelta(delta);
- }
await _nativeSyncApi.checkpointSync();
} catch (e, s) {
_log.severe("Error performing device sync", e, s);
@@ -93,6 +99,10 @@ class LocalSyncService {
try {
final Stopwatch stopwatch = Stopwatch()..start();
+ if (CurrentPlatform.isAndroid) {
+ await _syncDeviceTrashSnapshot();
+ }
+
final deviceAlbums = await _nativeSyncApi.getAlbums();
final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id});
@@ -104,9 +114,6 @@ class LocalSyncService {
onlyFirst: removeAlbum,
onlySecond: addAlbum,
);
- if (_trashSyncService.isAutoSyncMode) {
- await _trashSyncService.syncDeviceTrashSnapshot();
- }
await _nativeSyncApi.checkpointSync();
stopwatch.stop();
@@ -286,6 +293,53 @@ class LocalSyncService {
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt);
}
+
+ Future _applyTrashDelta(SyncDelta delta) async {
+ final trashUpdates = delta.updates;
+ if (trashUpdates.isEmpty) {
+ return Future.value();
+ }
+ final trashedAssets = [];
+ //todo try to reuse exist checksums from local assets table before they updated
+ for (final update in trashUpdates) {
+ final albums = delta.assetAlbums.cast>();
+ for (final String id in albums[update.id]!.cast().nonNulls) {
+ trashedAssets.add(update.toTrashedAsset(id));
+ }
+ }
+ _log.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.id)}");
+ await _trashedLocalAssetRepository.saveTrashedAssets(trashedAssets);
+ await _applyRemoteRestoreToLocal();
+ }
+
+ Future _syncDeviceTrashSnapshot() async {
+ final backupAlbums = await _localAlbumRepository.getBackupAlbums();
+ if (backupAlbums.isEmpty) {
+ _log.info("syncDeviceTrashSnapshot, No backup albums found");
+ return;
+ }
+ for (final album in backupAlbums) {
+ _log.info("syncDeviceTrashSnapshot prepare, album: ${album.id}/${album.name}");
+ final trashedPlatformAssets = await _nativeSyncApi.getTrashedAssetsForAlbum(album.id);
+ final trashedAssets = trashedPlatformAssets.toTrashedAssets(album.id);
+ await _trashedLocalAssetRepository.applyTrashSnapshot(trashedAssets, album.id);
+ }
+ await _applyRemoteRestoreToLocal();
+ }
+
+ Future _applyRemoteRestoreToLocal() async {
+ final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore();
+ if (remoteAssetsToRestore.isNotEmpty) {
+ _log.info("remoteAssetsToRestore: $remoteAssetsToRestore");
+ for (final asset in remoteAssetsToRestore) {
+ _log.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
+ await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index);
+ }
+ await _trashedLocalAssetRepository.restoreLocalAssets(remoteAssetsToRestore.map((e) => e.id));
+ } else {
+ _log.info("No remote assets found for restoration");
+ }
+ }
}
extension on Iterable {
@@ -320,3 +374,26 @@ extension on Iterable {
).toList();
}
}
+
+extension on PlatformAsset {
+ TrashedAsset toTrashedAsset(String albumId) => TrashedAsset(
+ id: id,
+ name: name,
+ checksum: null,
+ type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
+ createdAt: tryFromSecondsSinceEpoch(createdAt) ?? DateTime.now(),
+ updatedAt: tryFromSecondsSinceEpoch(updatedAt) ?? DateTime.now(),
+ albumId: albumId,
+ width: width,
+ height: height,
+ durationInSeconds: durationInSeconds,
+ isFavorite: isFavorite,
+ orientation: orientation,
+ );
+}
+
+extension PlatformAssetsExtension on Iterable {
+ Iterable toTrashedAssets(String albumId) {
+ return map((e) => e.toTrashedAsset(albumId));
+ }
+}
diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart
index b0f136a494..b4349de523 100644
--- a/mobile/lib/domain/services/sync_stream.service.dart
+++ b/mobile/lib/domain/services/sync_stream.service.dart
@@ -88,7 +88,7 @@ class SyncStreamService {
return _syncStreamRepository.deletePartnerV1(data.cast());
case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast();
- if (_trashSyncService.isAutoSyncMode) {
+ if (_trashSyncService.isTrashSyncMode) {
await _trashSyncService.handleRemoteTrashed(
remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum),
);
diff --git a/mobile/lib/domain/services/trash_sync.service.dart b/mobile/lib/domain/services/trash_sync.service.dart
index 37caad34c3..1fa7858ee8 100644
--- a/mobile/lib/domain/services/trash_sync.service.dart
+++ b/mobile/lib/domain/services/trash_sync.service.dart
@@ -1,23 +1,14 @@
-import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
-import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
-import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
-import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:logging/logging.dart';
-typedef TrashSyncItem = ({String remoteId, String checksum, DateTime? deletedAt});
-
class TrashSyncService {
final AppSettingsService _appSettingsService;
- final NativeSyncApi _nativeSyncApi;
final DriftLocalAssetRepository _localAssetRepository;
- final DriftLocalAlbumRepository _localAlbumRepository;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager;
final StorageRepository _storageRepository;
@@ -25,66 +16,25 @@ class TrashSyncService {
TrashSyncService({
required AppSettingsService appSettingsService,
- required NativeSyncApi nativeSyncApi,
required DriftLocalAssetRepository localAssetRepository,
- required DriftLocalAlbumRepository localAlbumRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
}) : _appSettingsService = appSettingsService,
- _nativeSyncApi = nativeSyncApi,
_localAssetRepository = localAssetRepository,
- _localAlbumRepository = localAlbumRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository;
- bool get isAutoSyncMode =>
+ bool get isTrashSyncMode =>
CurrentPlatform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid);
- Future updateChecksums(Iterable assets) async =>
- _trashedLocalAssetRepository.updateChecksums(assets);
-
- Future> getAssetsToHash(Iterable albumIds) async =>
- _trashedLocalAssetRepository.getToHash(albumIds);
-
- Future syncDeviceTrashSnapshot() async {
- final backupAlbums = await _localAlbumRepository.getBackupAlbums();
- if (backupAlbums.isEmpty) {
- _logger.info("No backup albums found");
- return;
- }
- for (final album in backupAlbums) {
- _logger.info("deviceTrashedAssets prepare, album: ${album.id}/${album.name}");
- final trashedPlatformAssets = await _nativeSyncApi.getTrashedAssetsForAlbum(album.id);
- final trashedAssets = trashedPlatformAssets.toTrashedAssets(album.id);
- await _trashedLocalAssetRepository.applyTrashSnapshot(trashedAssets, album.id);
- }
- await _applyRemoteRestoreToLocal();
- }
-
- Future applyTrashDelta(SyncDelta delta) async {
- final trashUpdates = delta.updates;
- if (trashUpdates.isEmpty) {
- return Future.value();
- }
- final trashedAssets = [];
- for (final update in trashUpdates) {
- final albums = delta.assetAlbums.cast>();
- for (final String id in albums[update.id]!.cast().nonNulls) {
- trashedAssets.add(update.toTrashedAsset(id));
- }
- }
- _logger.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.id)}");
- await _trashedLocalAssetRepository.saveTrashedAssets(trashedAssets);
- await _applyRemoteRestoreToLocal();
- }
Future handleRemoteTrashed(Iterable checksums) async {
if (checksums.isEmpty) {
return Future.value();
} else {
- final localAssetsToTrash = await _localAssetRepository.getBackupSelectedAssetsByAlbum(checksums);
+ final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
if (localAssetsToTrash.isNotEmpty) {
final mediaUrls = await Future.wait(
localAssetsToTrash.values
@@ -101,42 +51,5 @@ class TrashSyncService {
}
}
}
-
- Future _applyRemoteRestoreToLocal() async {
- final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore();
- if (remoteAssetsToRestore.isNotEmpty) {
- _logger.info("remoteAssetsToRestore: $remoteAssetsToRestore");
- for (final asset in remoteAssetsToRestore) {
- _logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
- await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index);
- }
- await _trashedLocalAssetRepository.restoreLocalAssets(remoteAssetsToRestore.map((e) => e.id));
- } else {
- _logger.info("No remote assets found for restoration");
- }
- }
}
-extension on PlatformAsset {
- TrashedAsset toTrashedAsset(String albumId) => TrashedAsset(
- id: id,
- name: name,
- checksum: null,
- type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
- createdAt: tryFromSecondsSinceEpoch(createdAt) ?? DateTime.now(),
- updatedAt: tryFromSecondsSinceEpoch(updatedAt) ?? DateTime.now(),
- volume: volume,
- albumId: albumId,
- width: width,
- height: height,
- durationInSeconds: durationInSeconds,
- isFavorite: isFavorite,
- orientation: orientation,
- );
-}
-
-extension PlatformAssetsExtension on Iterable {
- Iterable toTrashedAssets(String albumId) {
- return map((e) => e.toTrashedAsset(albumId));
- }
-}
diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart
index 94bca53121..10a4cefe66 100644
--- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart
+++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart
@@ -12,8 +12,6 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity
TextColumn get albumId => text()();
- TextColumn get volume => text().nullable()();
-
TextColumn get checksum => text().nullable()();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
@@ -28,7 +26,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
TrashedAsset toDto() => TrashedAsset(
id: id,
name: name,
- volume: volume,
albumId: albumId,
checksum: checksum,
type: type,
diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart
index eda44c256a..38df1218e0 100644
--- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart
+++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart
@@ -1,12 +1,12 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
-import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
- as i1;
+import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'
as i3;
-import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
+import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
+ as i1;
typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
i1.TrashedLocalAssetEntityCompanion Function({
@@ -19,7 +19,6 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
i0.Value durationInSeconds,
required String id,
required String albumId,
- i0.Value volume,
i0.Value checksum,
i0.Value isFavorite,
i0.Value orientation,
@@ -35,7 +34,6 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
i0.Value durationInSeconds,
i0.Value id,
i0.Value albumId,
- i0.Value volume,
i0.Value checksum,
i0.Value isFavorite,
i0.Value orientation,
@@ -97,11 +95,6 @@ class $$TrashedLocalAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
- i0.ColumnFilters get volume => $composableBuilder(
- column: $table.volume,
- builder: (column) => i0.ColumnFilters(column),
- );
-
i0.ColumnFilters get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnFilters(column),
@@ -173,11 +166,6 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
- i0.ColumnOrderings get volume => $composableBuilder(
- column: $table.volume,
- builder: (column) => i0.ColumnOrderings(column),
- );
-
i0.ColumnOrderings get checksum => $composableBuilder(
column: $table.checksum,
builder: (column) => i0.ColumnOrderings(column),
@@ -233,9 +221,6 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
i0.GeneratedColumn get albumId =>
$composableBuilder(column: $table.albumId, builder: (column) => column);
- i0.GeneratedColumn get volume =>
- $composableBuilder(column: $table.volume, builder: (column) => column);
-
i0.GeneratedColumn get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column);
@@ -305,7 +290,6 @@ class $$TrashedLocalAssetEntityTableTableManager
i0.Value durationInSeconds = const i0.Value.absent(),
i0.Value id = const i0.Value.absent(),
i0.Value albumId = const i0.Value.absent(),
- i0.Value volume = const i0.Value.absent(),
i0.Value checksum = const i0.Value.absent(),
i0.Value isFavorite = const i0.Value.absent(),
i0.Value orientation = const i0.Value.absent(),
@@ -319,7 +303,6 @@ class $$TrashedLocalAssetEntityTableTableManager
durationInSeconds: durationInSeconds,
id: id,
albumId: albumId,
- volume: volume,
checksum: checksum,
isFavorite: isFavorite,
orientation: orientation,
@@ -335,7 +318,6 @@ class $$TrashedLocalAssetEntityTableTableManager
i0.Value durationInSeconds = const i0.Value.absent(),
required String id,
required String albumId,
- i0.Value volume = const i0.Value.absent(),
i0.Value checksum = const i0.Value.absent(),
i0.Value isFavorite = const i0.Value.absent(),
i0.Value orientation = const i0.Value.absent(),
@@ -349,7 +331,6 @@ class $$TrashedLocalAssetEntityTableTableManager
durationInSeconds: durationInSeconds,
id: id,
albumId: albumId,
- volume: volume,
checksum: checksum,
isFavorite: isFavorite,
orientation: orientation,
@@ -499,17 +480,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
- static const i0.VerificationMeta _volumeMeta = const i0.VerificationMeta(
- 'volume',
- );
- @override
- late final i0.GeneratedColumn volume = i0.GeneratedColumn(
- 'volume',
- aliasedName,
- true,
- type: i0.DriftSqlType.string,
- requiredDuringInsert: false,
- );
static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta(
'checksum',
);
@@ -559,7 +529,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
durationInSeconds,
id,
albumId,
- volume,
checksum,
isFavorite,
orientation,
@@ -630,12 +599,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
} else if (isInserting) {
context.missing(_albumIdMeta);
}
- if (data.containsKey('volume')) {
- context.handle(
- _volumeMeta,
- volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta),
- );
- }
if (data.containsKey('checksum')) {
context.handle(
_checksumMeta,
@@ -707,10 +670,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
i0.DriftSqlType.string,
data['${effectivePrefix}album_id'],
)!,
- volume: attachedDatabase.typeMapping.read(
- i0.DriftSqlType.string,
- data['${effectivePrefix}volume'],
- ),
checksum: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}checksum'],
@@ -750,7 +709,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
final int? durationInSeconds;
final String id;
final String albumId;
- final String? volume;
final String? checksum;
final bool isFavorite;
final int orientation;
@@ -764,7 +722,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
this.durationInSeconds,
required this.id,
required this.albumId,
- this.volume,
this.checksum,
required this.isFavorite,
required this.orientation,
@@ -791,9 +748,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
}
map['id'] = i0.Variable(id);
map['album_id'] = i0.Variable(albumId);
- if (!nullToAbsent || volume != null) {
- map['volume'] = i0.Variable(volume);
- }
if (!nullToAbsent || checksum != null) {
map['checksum'] = i0.Variable(checksum);
}
@@ -819,7 +773,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
durationInSeconds: serializer.fromJson(json['durationInSeconds']),
id: serializer.fromJson(json['id']),
albumId: serializer.fromJson(json['albumId']),
- volume: serializer.fromJson(json['volume']),
checksum: serializer.fromJson(json['checksum']),
isFavorite: serializer.fromJson(json['isFavorite']),
orientation: serializer.fromJson(json['orientation']),
@@ -840,7 +793,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
'durationInSeconds': serializer.toJson(durationInSeconds),
'id': serializer.toJson(id),
'albumId': serializer.toJson(albumId),
- 'volume': serializer.toJson(volume),
'checksum': serializer.toJson(checksum),
'isFavorite': serializer.toJson(isFavorite),
'orientation': serializer.toJson(orientation),
@@ -857,7 +809,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
i0.Value durationInSeconds = const i0.Value.absent(),
String? id,
String? albumId,
- i0.Value volume = const i0.Value.absent(),
i0.Value checksum = const i0.Value.absent(),
bool? isFavorite,
int? orientation,
@@ -873,7 +824,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
: this.durationInSeconds,
id: id ?? this.id,
albumId: albumId ?? this.albumId,
- volume: volume.present ? volume.value : this.volume,
checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
@@ -893,7 +843,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
: this.durationInSeconds,
id: data.id.present ? data.id.value : this.id,
albumId: data.albumId.present ? data.albumId.value : this.albumId,
- volume: data.volume.present ? data.volume.value : this.volume,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite: data.isFavorite.present
? data.isFavorite.value
@@ -916,7 +865,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('albumId: $albumId, ')
- ..write('volume: $volume, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation')
@@ -935,7 +883,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
durationInSeconds,
id,
albumId,
- volume,
checksum,
isFavorite,
orientation,
@@ -953,7 +900,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
other.durationInSeconds == this.durationInSeconds &&
other.id == this.id &&
other.albumId == this.albumId &&
- other.volume == this.volume &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite &&
other.orientation == this.orientation);
@@ -970,7 +916,6 @@ class TrashedLocalAssetEntityCompanion
final i0.Value durationInSeconds;
final i0.Value id;
final i0.Value albumId;
- final i0.Value volume;
final i0.Value checksum;
final i0.Value isFavorite;
final i0.Value orientation;
@@ -984,7 +929,6 @@ class TrashedLocalAssetEntityCompanion
this.durationInSeconds = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.albumId = const i0.Value.absent(),
- this.volume = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
@@ -999,7 +943,6 @@ class TrashedLocalAssetEntityCompanion
this.durationInSeconds = const i0.Value.absent(),
required String id,
required String albumId,
- this.volume = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
@@ -1017,7 +960,6 @@ class TrashedLocalAssetEntityCompanion
i0.Expression? durationInSeconds,
i0.Expression? id,
i0.Expression? albumId,
- i0.Expression? volume,
i0.Expression? checksum,
i0.Expression? isFavorite,
i0.Expression? orientation,
@@ -1032,7 +974,6 @@ class TrashedLocalAssetEntityCompanion
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (id != null) 'id': id,
if (albumId != null) 'album_id': albumId,
- if (volume != null) 'volume': volume,
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
if (orientation != null) 'orientation': orientation,
@@ -1049,7 +990,6 @@ class TrashedLocalAssetEntityCompanion
i0.Value? durationInSeconds,
i0.Value? id,
i0.Value? albumId,
- i0.Value? volume,
i0.Value? checksum,
i0.Value? isFavorite,
i0.Value? orientation,
@@ -1064,7 +1004,6 @@ class TrashedLocalAssetEntityCompanion
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
id: id ?? this.id,
albumId: albumId ?? this.albumId,
- volume: volume ?? this.volume,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
@@ -1103,9 +1042,6 @@ class TrashedLocalAssetEntityCompanion
if (albumId.present) {
map['album_id'] = i0.Variable(albumId.value);
}
- if (volume.present) {
- map['volume'] = i0.Variable(volume.value);
- }
if (checksum.present) {
map['checksum'] = i0.Variable(checksum.value);
}
@@ -1130,7 +1066,6 @@ class TrashedLocalAssetEntityCompanion
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('albumId: $albumId, ')
- ..write('volume: $volume, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation')
diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart
index 3094b6660d..c8f45df6f9 100644
--- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart
+++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart
@@ -100,26 +100,27 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return query.map((localAlbum) => localAlbum.toDto()).get();
}
- Future