mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
use checksum for asset restoration
refactro code
This commit is contained in:
parent
b8274c9ed4
commit
40ac65db46
5 changed files with 82 additions and 40 deletions
|
|
@ -24,6 +24,7 @@ import java.security.MessageDigest
|
|||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.*
|
||||
import androidx.core.net.toUri
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `BackgroundService` and file trash operations
|
||||
|
|
@ -155,14 +156,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||
"restoreFromTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
val type = call.argument<Int>("type")
|
||||
if (fileName != null && type != null) {
|
||||
val checksum = call.argument<String>("checksum")
|
||||
if (fileName != null && type != null && checksum != null) {
|
||||
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
|
||||
restoreFromTrash(fileName, type, result)
|
||||
restoreFromTrash(fileName, type, checksum, result)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Media permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
result.error("INVALID_NAME", "The file name or checksum is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,14 +214,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun restoreFromTrash(name: String, type: Int, result: Result) {
|
||||
val uri = getTrashedFileUri(name, type)
|
||||
private fun restoreFromTrash(name: String, type: Int, checksum: String, result: Result) {
|
||||
val uri = getTrashedFileUri(name, type, checksum)
|
||||
if (uri == null) {
|
||||
Log.e("TrashError", "Asset Uri cannot be found obtained")
|
||||
Log.e(TAG, "TrashError, Asset Uri cannot be found obtained")
|
||||
result.error("TrashError", "Asset Uri cannot be found obtained", null)
|
||||
return
|
||||
}
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
Log.i(TAG, "trying to restore from trash FILE_URI - $uri")
|
||||
uri.let { toggleTrash(listOf(it), false, result) }
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +233,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||
result.error("TrashError", "Activity or ContentResolver not available", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
|
||||
pendingResult = result // Store for onActivityResult
|
||||
|
|
@ -241,39 +242,68 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||
null, 0, 0, 0
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error creating or starting trash request", e)
|
||||
Log.e(TAG, "TrashError, Error creating or starting trash request", e)
|
||||
result.error("TrashError", "Error creating or starting trash request", null)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun getTrashedFileUri(fileName: String, type: Int): Uri? {
|
||||
private fun getTrashedFileUri(fileName: String, type: Int, checksum: String): Uri? {
|
||||
val contentResolver = context?.contentResolver ?: return null
|
||||
val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
|
||||
val projection = arrayOf(
|
||||
MediaStore.Files.FileColumns._ID,
|
||||
MediaStore.Files.FileColumns.DISPLAY_NAME
|
||||
)
|
||||
|
||||
val dotIndex = fileName.lastIndexOf('.')
|
||||
val baseName = if (dotIndex != -1) fileName.substring(0, dotIndex) else fileName
|
||||
val extension = if (dotIndex != -1) fileName.substring(dotIndex) else ""
|
||||
val nameLike = "%${baseName}%${extension}"
|
||||
|
||||
val queryArgs = Bundle().apply {
|
||||
putString(
|
||||
ContentResolver.QUERY_ARG_SQL_SELECTION,
|
||||
"${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
|
||||
)
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
|
||||
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} LIKE ?")
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(nameLike))
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||
}
|
||||
|
||||
val expectedBytes = try {
|
||||
android.util.Base64.decode(checksum, android.util.Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getTrashedFileUri, invalid Base64 checksum: $checksum, Exception: $e")
|
||||
return null
|
||||
}
|
||||
|
||||
contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
||||
// same order as AssetType from dart
|
||||
val contentUri = when (type) {
|
||||
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
else -> queryUri
|
||||
val idxId = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||
val idxName = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idxId)
|
||||
val dn = cursor.getString(idxName)
|
||||
|
||||
val candidateUri = ContentUris.withAppendedId(queryUri, id)
|
||||
|
||||
val sha1Bytes = try {
|
||||
contentResolver.openInputStream(candidateUri)?.use { ins -> sha1OfStream(ins) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getTrashedFileUri, hash failed for $dn: ${e.message}")
|
||||
null
|
||||
} ?: continue
|
||||
|
||||
if (sha1Bytes.contentEquals(expectedBytes)) {
|
||||
// same order as AssetType from dart
|
||||
val contentUri = when (type) {
|
||||
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
else -> queryUri
|
||||
}
|
||||
return ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
return ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "getTrashedFileUri, not found by checksum, nameLike=$nameLike, checksum: $checksum")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +345,18 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun sha1OfStream(ins: InputStream): ByteArray {
|
||||
val buf = ByteArray(BUFFER_SIZE)
|
||||
val md = MessageDigest.getInstance("SHA-1")
|
||||
var len: Int
|
||||
while (true) {
|
||||
len = ins.read(buf)
|
||||
if (len <= 0) break
|
||||
md.update(buf, 0, len)
|
||||
}
|
||||
return md.digest()
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
|
|
|
|||
|
|
@ -52,20 +52,16 @@ class TrashSyncService {
|
|||
if (trashedAssetsChecksums.isEmpty) {
|
||||
return Future.value();
|
||||
} else {
|
||||
final candidatesToTrash = await _localAssetRepository.getByChecksums(trashedAssetsChecksums);
|
||||
if (candidatesToTrash.isNotEmpty) {
|
||||
final groupedByChecksum = <String, LocalAsset>{};
|
||||
for (final localAsset in candidatesToTrash) {
|
||||
groupedByChecksum[localAsset.checksum!] = localAsset;
|
||||
}
|
||||
final localAssetsToTrash = await _localAssetRepository.getByChecksums(trashedAssetsChecksums);
|
||||
if (localAssetsToTrash.isNotEmpty) {
|
||||
final mediaUrls = await Future.wait(
|
||||
groupedByChecksum.values.map(
|
||||
localAssetsToTrash.map(
|
||||
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
|
||||
),
|
||||
);
|
||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
||||
await _localAssetRepository.delete(candidatesToTrash.map((asset) => asset.id));
|
||||
await _localAssetRepository.delete(localAssetsToTrash.map((asset) => asset.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,9 +75,9 @@ class TrashSyncService {
|
|||
isTrashed: true,
|
||||
);
|
||||
if (remoteAssetsToRestore.isNotEmpty) {
|
||||
_logger.info("Restoring from trash ${remoteAssetsToRestore.map((e) => e.name).join(", ")} assets");
|
||||
_logger.info("Restoring from trash ${remoteAssetsToRestore.map((e) => "${e.name}/${e.checksum}").join(", ")} assets");
|
||||
for (RemoteAsset asset in remoteAssetsToRestore) {
|
||||
await _localFilesManager.restoreFromTrash(asset.name, asset.type.index);
|
||||
await _localFilesManager.restoreFromTrash(asset.name, asset.type.index, asset.checksum!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ class LocalFilesManagerRepository {
|
|||
return await _service.moveToTrash(mediaUrls);
|
||||
}
|
||||
|
||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
||||
return await _service.restoreFromTrash(fileName, type);
|
||||
Future<bool> restoreFromTrash(String fileName, int type, String checksum) async {
|
||||
return await _service.restoreFromTrash(fileName, type, checksum);
|
||||
}
|
||||
|
||||
Future<bool> requestManageMediaPermission() async {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ class LocalFilesManagerService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
||||
Future<bool> restoreFromTrash(String fileName, int type, String checksum) async {
|
||||
try {
|
||||
return await _channel.invokeMethod('restoreFromTrash', {'fileName': fileName, 'type': type});
|
||||
return await _channel.invokeMethod('restoreFromTrash', {
|
||||
'fileName': fileName,
|
||||
'type': type,
|
||||
'checksum': checksum,
|
||||
});
|
||||
} catch (e, s) {
|
||||
_logger.warning('Error restore file from trash', e, s);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -689,7 +689,7 @@ class SyncService {
|
|||
}
|
||||
trashMediaUrls.add(mediaUrl);
|
||||
} else {
|
||||
await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index);
|
||||
await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index, asset.checksum);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue