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 java.io.FileInputStream
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android plugin for Dart `BackgroundService` and file trash operations
|
* Android plugin for Dart `BackgroundService` and file trash operations
|
||||||
|
|
@ -155,14 +156,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||||
"restoreFromTrash" -> {
|
"restoreFromTrash" -> {
|
||||||
val fileName = call.argument<String>("fileName")
|
val fileName = call.argument<String>("fileName")
|
||||||
val type = call.argument<Int>("type")
|
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()) {
|
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
|
||||||
restoreFromTrash(fileName, type, result)
|
restoreFromTrash(fileName, type, checksum, result)
|
||||||
} else {
|
} else {
|
||||||
result.error("PERMISSION_DENIED", "Media permission required", null)
|
result.error("PERMISSION_DENIED", "Media permission required", null)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun restoreFromTrash(name: String, type: Int, result: Result) {
|
private fun restoreFromTrash(name: String, type: Int, checksum: String, result: Result) {
|
||||||
val uri = getTrashedFileUri(name, type)
|
val uri = getTrashedFileUri(name, type, checksum)
|
||||||
if (uri == null) {
|
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)
|
result.error("TrashError", "Asset Uri cannot be found obtained", null)
|
||||||
return
|
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) }
|
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)
|
result.error("TrashError", "Activity or ContentResolver not available", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
|
val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
|
||||||
pendingResult = result // Store for onActivityResult
|
pendingResult = result // Store for onActivityResult
|
||||||
|
|
@ -241,39 +242,68 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||||
null, 0, 0, 0
|
null, 0, 0, 0
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} 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)
|
result.error("TrashError", "Error creating or starting trash request", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@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 contentResolver = context?.contentResolver ?: return null
|
||||||
val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
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 {
|
val queryArgs = Bundle().apply {
|
||||||
putString(
|
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} LIKE ?")
|
||||||
ContentResolver.QUERY_ARG_SQL_SELECTION,
|
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(nameLike))
|
||||||
"${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
|
|
||||||
)
|
|
||||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
|
|
||||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
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 ->
|
contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
val idxId = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
|
||||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
val idxName = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
|
||||||
// same order as AssetType from dart
|
|
||||||
val contentUri = when (type) {
|
while (cursor.moveToNext()) {
|
||||||
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
val id = cursor.getLong(idxId)
|
||||||
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
val dn = cursor.getString(idxName)
|
||||||
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
|
||||||
else -> queryUri
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,6 +345,18 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||||
}
|
}
|
||||||
return false
|
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"
|
private const val TAG = "BackgroundServicePlugin"
|
||||||
|
|
|
||||||
|
|
@ -52,20 +52,16 @@ class TrashSyncService {
|
||||||
if (trashedAssetsChecksums.isEmpty) {
|
if (trashedAssetsChecksums.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
} else {
|
} else {
|
||||||
final candidatesToTrash = await _localAssetRepository.getByChecksums(trashedAssetsChecksums);
|
final localAssetsToTrash = await _localAssetRepository.getByChecksums(trashedAssetsChecksums);
|
||||||
if (candidatesToTrash.isNotEmpty) {
|
if (localAssetsToTrash.isNotEmpty) {
|
||||||
final groupedByChecksum = <String, LocalAsset>{};
|
|
||||||
for (final localAsset in candidatesToTrash) {
|
|
||||||
groupedByChecksum[localAsset.checksum!] = localAsset;
|
|
||||||
}
|
|
||||||
final mediaUrls = await Future.wait(
|
final mediaUrls = await Future.wait(
|
||||||
groupedByChecksum.values.map(
|
localAssetsToTrash.map(
|
||||||
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
|
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
||||||
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
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,
|
isTrashed: true,
|
||||||
);
|
);
|
||||||
if (remoteAssetsToRestore.isNotEmpty) {
|
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) {
|
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);
|
return await _service.moveToTrash(mediaUrls);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
Future<bool> restoreFromTrash(String fileName, int type, String checksum) async {
|
||||||
return await _service.restoreFromTrash(fileName, type);
|
return await _service.restoreFromTrash(fileName, type, checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> requestManageMediaPermission() async {
|
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 {
|
try {
|
||||||
return await _channel.invokeMethod('restoreFromTrash', {'fileName': fileName, 'type': type});
|
return await _channel.invokeMethod('restoreFromTrash', {
|
||||||
|
'fileName': fileName,
|
||||||
|
'type': type,
|
||||||
|
'checksum': checksum,
|
||||||
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_logger.warning('Error restore file from trash', e, s);
|
_logger.warning('Error restore file from trash', e, s);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -689,7 +689,7 @@ class SyncService {
|
||||||
}
|
}
|
||||||
trashMediaUrls.add(mediaUrl);
|
trashMediaUrls.add(mediaUrl);
|
||||||
} else {
|
} 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