diff --git a/app/src/main/java/com/example/research/common/progress/DictionaryPipelineCoordinator.kt b/app/src/main/java/com/example/research/common/progress/DictionaryPipelineCoordinator.kt index 74e3591..883f038 100644 --- a/app/src/main/java/com/example/research/common/progress/DictionaryPipelineCoordinator.kt +++ b/app/src/main/java/com/example/research/common/progress/DictionaryPipelineCoordinator.kt @@ -61,7 +61,7 @@ class DictionaryPipelineCoordinator( private fun runPipeline(isImport: Boolean) { pipelineJob?.cancel() 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() try { triggerReindexing(isImport) @@ -83,6 +83,7 @@ class DictionaryPipelineCoordinator( } cleanupNewFiles(dir, filesBeforeReindex) } + cleanupOrphanedFiles(dir) throw e } catch (e: Exception) { 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) { try { dir?.listFiles()?.forEach { file -> diff --git a/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt b/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt index d367e42..be7d581 100644 --- a/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt +++ b/app/src/main/java/com/example/research/data/repository/LocalDictionaryRepository.kt @@ -390,12 +390,15 @@ class LocalDictionaryRepository( } ?: emptyList() } - private fun isDictionaryPayloadFile(file: File): Boolean = - file.isFile && ( - file.name.endsWith(".dsl") || - file.name.endsWith(".dsl.dz") || - file.name.endsWith(".dsl.gz") - ) + private fun isDictionaryPayloadFile(file: File): Boolean { + if (!file.isFile) return false + val n = file.name.lowercase() + if (n.endsWith(".dsl.dz") || n.endsWith(".dsl.gz")) return true + if (n.endsWith(".dsl")) { + return File(file.parent, file.name + ".dz").exists() + } + return false + } suspend fun search(query: String): OperationResult> = withContext(defaultDispatcher) { if (query.isBlank()) return@withContext OperationResult.Success(emptyList()) diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryDownloader.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryDownloader.kt index 06d193c..981476b 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryDownloader.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryDownloader.kt @@ -15,8 +15,11 @@ import com.example.research.feature.download.config.DictionaryConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.isActive import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.coroutines.yield @@ -58,20 +61,22 @@ class DictionaryDownloader( ) { if (fileUri == null) return - runCatching { - val fileToDelete = fileStorageManager.getDocumentFile(fileUri) - if (fileToDelete != null && fileToDelete.exists()) { - val deleted = fileStorageManager.forceDeleteFile(fileToDelete, fileName) - if (!deleted) { - fileStorageManager.forceDeleteFileByName(folderUri, fileName) - } - } - }.onFailure { e -> - Log.e(TAG, "Failed to delete partial file: $fileUri", e) + withContext(NonCancellable) { runCatching { - fileStorageManager.forceDeleteFileByName(folderUri, fileName) - }.onFailure { deleteError -> - Log.e(TAG, "Failed to delete file by name: $fileName", deleteError) + val fileToDelete = fileStorageManager.getDocumentFile(fileUri) + if (fileToDelete != null && fileToDelete.exists()) { + val deleted = fileStorageManager.forceDeleteFile(fileToDelete, fileName) + if (!deleted) { + fileStorageManager.forceDeleteFileByName(folderUri, fileName) + } + } + }.onFailure { e -> + Log.e(TAG, "Failed to delete partial file: $fileUri", e) + runCatching { + fileStorageManager.forceDeleteFileByName(folderUri, fileName) + }.onFailure { deleteError -> + Log.e(TAG, "Failed to delete file by name: $fileName", deleteError) + } } } } @@ -169,7 +174,7 @@ class DictionaryDownloader( } private suspend fun deletePartialFileIfExists(folderUri: Uri, fileName: String) { - withContext(Dispatchers.IO) { + withContext(Dispatchers.IO + NonCancellable) { runCatching { val existingFile = fileStorageManager.findFile(folderUri, fileName) if (existingFile != null && existingFile.exists()) { @@ -313,7 +318,7 @@ class DictionaryDownloader( call?.cancel() response?.close() outputStream?.close() - if (e is CancellationException) { + if (e is CancellationException || !currentCoroutineContext().isActive) { deletePartialFileIfExists(folderUri, fileName) deletePartialFileSafely(fileUri, folderUri, fileName) throw e diff --git a/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt b/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt index 9342d8f..0547ca3 100644 --- a/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt +++ b/app/src/main/java/com/example/research/feature/download/repository/DictionaryExtractor.kt @@ -9,7 +9,9 @@ import com.example.research.common.util.FileStorageManager import com.example.research.data.dictzip.DictZipRandomAccessFile import com.example.research.data.dictzip.DictZipWriter import com.example.research.feature.download.config.DictionaryConfig +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext class DictionaryExtractor( @@ -138,11 +140,39 @@ class DictionaryExtractor( onProgress?.invoke(1f) Result.success(Unit) + } catch (e: CancellationException) { + deleteOrphanedFiles(folderUri) + throw e } catch (e: Exception) { 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( dslUri: Uri, dslFileName: String,