fix: clean up orphaned and partial files after cancelled downloads
Treat inactive download contexts as cancellation, delete the partial file, and rethrow the cancellation so the pipeline does not retain an incomplete payload. Also remove orphaned .dsl/.dz files left behind by cancelled downloads. Run the partial/orphan deletions on NonCancellable: the cleanup is invoked from already-cancelled coroutines, where withContext(Dispatchers.IO) aborts before executing the block and silently leaves the files behind.
This commit is contained in:
1 parent
8319973161
commit
edf6a6074e
4 files changed
+67
-9
No files matched your search
+21
-1
@@ -61,7 +61,7 @@ class DictionaryPipelineCoordinator(
|
|||||||
private fun runPipeline(isImport: Boolean) {
|
private fun runPipeline(isImport: Boolean) {
|
||||||
pipelineJob?.cancel()
|
pipelineJob?.cancel()
|
||||||
pipelineJob = scope.launch(launchDispatcher) {
|
pipelineJob = scope.launch(launchDispatcher) {
|
||||||
val dir = if (isImport) File(dictionaryPath()) else null
|
val dir = File(dictionaryPath())
|
||||||
val filesBeforeReindex = dir?.listFiles()?.map { it.name }?.toSet() ?: emptySet()
|
val filesBeforeReindex = dir?.listFiles()?.map { it.name }?.toSet() ?: emptySet()
|
||||||
try {
|
try {
|
||||||
triggerReindexing(isImport)
|
triggerReindexing(isImport)
|
||||||
@@ -83,6 +83,7 @@ class DictionaryPipelineCoordinator(
|
|||||||
}
|
}
|
||||||
cleanupNewFiles(dir, filesBeforeReindex)
|
cleanupNewFiles(dir, filesBeforeReindex)
|
||||||
}
|
}
|
||||||
|
cleanupOrphanedFiles(dir)
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ui.showError()
|
ui.showError()
|
||||||
@@ -115,6 +116,25 @@ class DictionaryPipelineCoordinator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanupOrphanedFiles(dir: File?) {
|
||||||
|
try {
|
||||||
|
dir?.listFiles()?.forEach { file ->
|
||||||
|
val n = file.name.lowercase()
|
||||||
|
val isRawDsl = n.endsWith(".dsl") && !n.endsWith(".dsl.dz") && !n.endsWith(".dsl.gz")
|
||||||
|
val isBareDz = n.endsWith(".dz") && !n.endsWith(".dsl.dz")
|
||||||
|
val orphaned = when {
|
||||||
|
isBareDz -> true
|
||||||
|
isRawDsl -> !File(file.parent, file.name + ".dz").exists()
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
if (orphaned) {
|
||||||
|
try { file.delete() } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun cleanupNewFiles(dir: File?, filesBeforeSnapshot: Set<String>) {
|
private fun cleanupNewFiles(dir: File?, filesBeforeSnapshot: Set<String>) {
|
||||||
try {
|
try {
|
||||||
dir?.listFiles()?.forEach { file ->
|
dir?.listFiles()?.forEach { file ->
|
||||||
|
|||||||
@@ -390,12 +390,15 @@ class LocalDictionaryRepository(
|
|||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDictionaryPayloadFile(file: File): Boolean =
|
private fun isDictionaryPayloadFile(file: File): Boolean {
|
||||||
file.isFile && (
|
if (!file.isFile) return false
|
||||||
file.name.endsWith(".dsl") ||
|
val n = file.name.lowercase()
|
||||||
file.name.endsWith(".dsl.dz") ||
|
if (n.endsWith(".dsl.dz") || n.endsWith(".dsl.gz")) return true
|
||||||
file.name.endsWith(".dsl.gz")
|
if (n.endsWith(".dsl")) {
|
||||||
)
|
return File(file.parent, file.name + ".dz").exists()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun search(query: String): OperationResult<List<IndexEntry>> = withContext(defaultDispatcher) {
|
suspend fun search(query: String): OperationResult<List<IndexEntry>> = withContext(defaultDispatcher) {
|
||||||
if (query.isBlank()) return@withContext OperationResult.Success(emptyList())
|
if (query.isBlank()) return@withContext OperationResult.Success(emptyList())
|
||||||
|
|||||||
+7
-2
@@ -15,8 +15,11 @@ import com.example.research.feature.download.config.DictionaryConfig
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
@@ -58,6 +61,7 @@ class DictionaryDownloader(
|
|||||||
) {
|
) {
|
||||||
if (fileUri == null) return
|
if (fileUri == null) return
|
||||||
|
|
||||||
|
withContext(NonCancellable) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val fileToDelete = fileStorageManager.getDocumentFile(fileUri)
|
val fileToDelete = fileStorageManager.getDocumentFile(fileUri)
|
||||||
if (fileToDelete != null && fileToDelete.exists()) {
|
if (fileToDelete != null && fileToDelete.exists()) {
|
||||||
@@ -75,6 +79,7 @@ class DictionaryDownloader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
suspend fun downloadDictionaries(
|
suspend fun downloadDictionaries(
|
||||||
folderUri: Uri,
|
folderUri: Uri,
|
||||||
sources: List<DictionarySource>,
|
sources: List<DictionarySource>,
|
||||||
@@ -169,7 +174,7 @@ class DictionaryDownloader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deletePartialFileIfExists(folderUri: Uri, fileName: String) {
|
private suspend fun deletePartialFileIfExists(folderUri: Uri, fileName: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO + NonCancellable) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val existingFile = fileStorageManager.findFile(folderUri, fileName)
|
val existingFile = fileStorageManager.findFile(folderUri, fileName)
|
||||||
if (existingFile != null && existingFile.exists()) {
|
if (existingFile != null && existingFile.exists()) {
|
||||||
@@ -313,7 +318,7 @@ class DictionaryDownloader(
|
|||||||
call?.cancel()
|
call?.cancel()
|
||||||
response?.close()
|
response?.close()
|
||||||
outputStream?.close()
|
outputStream?.close()
|
||||||
if (e is CancellationException) {
|
if (e is CancellationException || !currentCoroutineContext().isActive) {
|
||||||
deletePartialFileIfExists(folderUri, fileName)
|
deletePartialFileIfExists(folderUri, fileName)
|
||||||
deletePartialFileSafely(fileUri, folderUri, fileName)
|
deletePartialFileSafely(fileUri, folderUri, fileName)
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
+30
@@ -9,7 +9,9 @@ import com.example.research.common.util.FileStorageManager
|
|||||||
import com.example.research.data.dictzip.DictZipRandomAccessFile
|
import com.example.research.data.dictzip.DictZipRandomAccessFile
|
||||||
import com.example.research.data.dictzip.DictZipWriter
|
import com.example.research.data.dictzip.DictZipWriter
|
||||||
import com.example.research.feature.download.config.DictionaryConfig
|
import com.example.research.feature.download.config.DictionaryConfig
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DictionaryExtractor(
|
class DictionaryExtractor(
|
||||||
@@ -138,11 +140,39 @@ class DictionaryExtractor(
|
|||||||
|
|
||||||
onProgress?.invoke(1f)
|
onProgress?.invoke(1f)
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
deleteOrphanedFiles(folderUri)
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteOrphanedFiles(folderUri: Uri) {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO + NonCancellable) {
|
||||||
|
val files = fileStorageManager.listFilesInFolder(folderUri)
|
||||||
|
for (file in files) {
|
||||||
|
val n = file.name.lowercase()
|
||||||
|
val isRawDsl = n.endsWith(DictionaryConfig.DSL_EXTENSION) &&
|
||||||
|
!n.endsWith(DictionaryConfig.DICTZIP_EXTENSION) &&
|
||||||
|
!n.endsWith(".dsl.gz")
|
||||||
|
val isBareDz = n.endsWith(".dz") && !n.endsWith(DictionaryConfig.DICTZIP_EXTENSION)
|
||||||
|
when {
|
||||||
|
isBareDz -> ArchiveUtils.deleteFile(context, file.uri)
|
||||||
|
isRawDsl -> {
|
||||||
|
val pair = fileStorageManager.findFile(folderUri, file.name + ".dz")
|
||||||
|
if (pair == null || !pair.exists()) {
|
||||||
|
ArchiveUtils.deleteFile(context, file.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun ensureDslDictZip(
|
private suspend fun ensureDslDictZip(
|
||||||
dslUri: Uri,
|
dslUri: Uri,
|
||||||
dslFileName: String,
|
dslFileName: String,
|
||||||
|
|||||||
Reference in new issue
Block a user