Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-1840: Fix project deletion to remove hidden backups and handle partial failures#588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
f67c5c60727bb3aaba6f56346cb40696f38File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||
| package com.itsaky.androidide.utils | ||||||||||||||||||||||
| import android.util.Log | ||||||||||||||||||||||
| import io.sentry.Sentry | ||||||||||||||||||||||
| import kotlinx.coroutines.* | ||||||||||||||||||||||
| import java.io.File | ||||||||||||||||||||||
| import java.io.IOException | ||||||||||||||||||||||
| @@ -9,34 +10,43 @@ object FileDeleteUtils { | ||||||||||||||||||||||
| private const val TAG = "FileDeleteUtils" | ||||||||||||||||||||||
| fun deleteRecursive( | ||||||||||||||||||||||
| fileOrDirectory: File, | ||||||||||||||||||||||
| onDeleted: (success: Boolean) -> Unit = {} | ||||||||||||||||||||||
| ) { | ||||||||||||||||||||||
| suspend fun deleteRecursive(fileOrDirectory: File): Boolean = withContext(Dispatchers.IO) { | ||||||||||||||||||||||
| val hiddenFile = File( | ||||||||||||||||||||||
| fileOrDirectory.parentFile, | ||||||||||||||||||||||
| ".${fileOrDirectory.name}" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| CoroutineScope(Dispatchers.IO).launch { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| if (fileOrDirectory.isDirectory) { | ||||||||||||||||||||||
| val copied = fileOrDirectory.copyRecursively(hiddenFile, overwrite = false) | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| fileOrDirectory.copyTo(hiddenFile, overwrite = false) | ||||||||||||||||||||||
| Log.d(TAG, "File copy complete") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } catch (t: Throwable) { | ||||||||||||||||||||||
| Log.e(TAG, "Copy failed: ${t.message}", t) | ||||||||||||||||||||||
| if (hiddenFile.exists() && !fileOrDirectory.exists()) { | ||||||||||||||||||||||
| Log.w(TAG, "Cleaning up previous failed deletion: ${hiddenFile.absolutePath}") | ||||||||||||||||||||||
| if (!deleteRecursively(hiddenFile)) { | ||||||||||||||||||||||
| Log.e(TAG, "Failed to clean up hidden file: ${hiddenFile.absolutePath}") | ||||||||||||||||||||||
| return@withContext false | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| val deletedOriginal = deleteRecursively(fileOrDirectory) | ||||||||||||||||||||||
| if (fileOrDirectory.exists()) { | ||||||||||||||||||||||
| val renamed = runCatching { | ||||||||||||||||||||||
| fileOrDirectory.renameTo(hiddenFile) | ||||||||||||||||||||||
| }.onFailure { t -> | ||||||||||||||||||||||
| Log.e(TAG, "Failed to rename ${fileOrDirectory.absolutePath} to hidden: ${t.message}", t) | ||||||||||||||||||||||
| Sentry.captureException(t) | ||||||||||||||||||||||
| }.getOrDefault(false) | ||||||||||||||||||||||
| withContext(Dispatchers.Main) { | ||||||||||||||||||||||
| onDeleted(deletedOriginal) | ||||||||||||||||||||||
| if (!renamed) { | ||||||||||||||||||||||
| return@withContext false | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } else if (!hiddenFile.exists()) { | ||||||||||||||||||||||
| Log.d(TAG, "File does not exist: ${fileOrDirectory.absolutePath}") | ||||||||||||||||||||||
| return@withContext true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
Comment on lines
+38
to
41
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Race condition: when
Suggested change
| ||||||||||||||||||||||
| val deleted = deleteRecursively(hiddenFile) | ||||||||||||||||||||||
| if (!deleted) { | ||||||||||||||||||||||
| Log.w(TAG, "Hidden directory remains after deletion failure: ${hiddenFile.absolutePath}") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| deleted | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| private fun deleteRecursively(file: File): Boolean { | ||||||||||||||||||||||
| @@ -45,23 +55,32 @@ object FileDeleteUtils { | ||||||||||||||||||||||
| val children = file.listFiles() | ||||||||||||||||||||||
| if (children != null) { | ||||||||||||||||||||||
| for (child in children) { | ||||||||||||||||||||||
| deleteRecursively(child) | ||||||||||||||||||||||
| if (!deleteRecursively(child)) { | ||||||||||||||||||||||
| Log.w(TAG, "Failed to delete: ${child.absolutePath}") | ||||||||||||||||||||||
| return false | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| val deleted = file.delete() | ||||||||||||||||||||||
| return deleted || !file.exists() | ||||||||||||||||||||||
| return file.delete().also { deleted -> | ||||||||||||||||||||||
| if (!deleted) { | ||||||||||||||||||||||
| Log.e(TAG, "Failed to delete: ${file.absolutePath}") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } catch (e: SecurityException) { | ||||||||||||||||||||||
| Log.e(TAG, "SecurityException deleting ${file.absolutePath}: ${e.message}", e) | ||||||||||||||||||||||
| return !file.exists() | ||||||||||||||||||||||
| Sentry.captureException(e) | ||||||||||||||||||||||
| return false | ||||||||||||||||||||||
| } catch (e: IOException) { | ||||||||||||||||||||||
| Log.e(TAG, "IOException deleting ${file.absolutePath}: ${e.message}", e) | ||||||||||||||||||||||
| return !file.exists() | ||||||||||||||||||||||
| Sentry.captureException(e) | ||||||||||||||||||||||
| return false | ||||||||||||||||||||||
| } catch (t: Throwable) { | ||||||||||||||||||||||
| Log.e(TAG, "Unexpected error deleting ${file.absolutePath}: ${t.message}", t) | ||||||||||||||||||||||
| return !file.exists() | ||||||||||||||||||||||
| Sentry.captureException(t) | ||||||||||||||||||||||
| return false | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Unnecessary dispatcher specification:
async(Dispatchers.IO)is redundant sinceMainActivity.deleteProject()already useswithContext(Dispatchers.IO)internally at FileDeleteUtils.kt:13. The explicit dispatcher causes an extra thread context switch.