Improve FileNotFoundException handling (#1332)

This commit is contained in:
Koitharu
2025-03-15 10:38:41 +02:00
parent f7c70577ae
commit 6a0ad7f79b
7 changed files with 61 additions and 17 deletions

View File

@@ -28,9 +28,9 @@ fun File.subdir(name: String) = File(this, name).also {
if (!it.exists()) it.mkdirs()
}
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
fun File.takeIfReadable() = takeIf { it.isReadable() }
fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() }
fun File.takeIfWriteable() = takeIf { it.isWriteable() }
fun File.isNotEmpty() = length() != 0L
@@ -110,3 +110,11 @@ fun File.walkCompat(includeDirectories: Boolean): Sequence<File> = if (Build.VER
val File.normalizedExtension: String?
get() = MimeTypes.getNormalizedExtension(name)
fun File.isReadable() = runCatching {
canRead()
}.getOrDefault(false)
fun File.isWriteable() = runCatching {
canWrite()
}.getOrDefault(false)

View File

@@ -41,6 +41,7 @@ import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import java.io.File
import java.net.ConnectException
import java.net.NoRouteToHostException
import java.net.SocketException
@@ -52,6 +53,8 @@ private const val MSG_NO_SPACE_LEFT = "No space left on device"
private const val MSG_CONNECTION_RESET = "Connection reset"
private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported"
private val FNFE_MESSAGE_REGEX = Regex("^(/[^\\s:]+)?.+?\\s([A-Z]{2,6})?\\s.+$")
fun Throwable.getDisplayMessage(resources: Resources): String = getDisplayMessageOrNull(resources)
?: resources.getString(R.string.error_occurred)
@@ -86,7 +89,7 @@ private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = w
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is BadBackupFormatException -> resources.getString(R.string.unsupported_backup_message)
is FileNotFoundException -> resources.getString(R.string.file_not_found)
is FileNotFoundException -> parseMessage(resources) ?: message
is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration)
@@ -225,3 +228,35 @@ fun Throwable.isWebViewUnavailable(): Boolean {
@Suppress("FunctionName")
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT)
fun FileNotFoundException.getFile(): File? {
val groups = FNFE_MESSAGE_REGEX.matchEntire(message ?: return null)?.groupValues ?: return null
return groups.getOrNull(1)?.let { File(it) }
}
fun FileNotFoundException.parseMessage(resources: Resources): String? {
/*
Examples:
/storage/0000-0000/Android/media/d1f08350-0c25-460b-8f50-008e49de3873.jpg.tmp: open failed: EROFS (Read-only file system)
/storage/emulated/0/Android/data/org.koitharu.kotatsu/cache/pages/fe06e192fa371e55918980f7a24c91ea.jpg: open failed: ENOENT (No such file or directory)
/storage/0000-0000/Android/data/org.koitharu.kotatsu/files/manga/e57d3af4-216e-48b2-8432-1541d58eea1e.tmp (I/O error)
*/
val groups = FNFE_MESSAGE_REGEX.matchEntire(message ?: return null)?.groupValues ?: return null
val path = groups.getOrNull(1)
val error = groups.getOrNull(2)
val baseMessageIs = when (error) {
"EROFS" -> R.string.no_write_permission_to_file
"ENOENT" -> R.string.file_not_found
else -> return null
}
return if (path.isNullOrEmpty()) {
resources.getString(baseMessageIs)
} else {
resources.getString(
R.string.inline_preference_pattern,
resources.getString(baseMessageIs),
path,
)
}
}

View File

@@ -17,7 +17,9 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.isWriteable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
import org.koitharu.kotatsu.core.util.ext.withChildren
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaParser
@@ -203,7 +205,7 @@ class LocalMangaRepository @Inject constructor(
override suspend fun getRelated(seed: Manga): List<Manga> = emptyList()
suspend fun getOutputDir(manga: Manga, fallback: File?): File? {
val defaultDir = fallback ?: storageManager.getDefaultWriteableDir()
val defaultDir = fallback?.takeIfWriteable() ?: storageManager.getDefaultWriteableDir()
if (defaultDir != null && LocalMangaOutput.get(defaultDir, manga) != null) {
return defaultDir
}

View File

@@ -22,7 +22,10 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isReadable
import org.koitharu.kotatsu.core.util.ext.isWriteable
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.io.File
import javax.inject.Inject
@@ -81,8 +84,8 @@ class LocalStorageManager @Inject constructor(
}
suspend fun getDefaultWriteableDir(): File? = runInterruptible(Dispatchers.IO) {
val preferredDir = settings.mangaStorageDir?.takeIf { it.isWriteable() }
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
val preferredDir = settings.mangaStorageDir?.takeIfWriteable()
preferredDir ?: getFallbackStorageDir()?.takeIfWriteable()
}
suspend fun getApplicationStorageDirs(): Set<File> = runInterruptible(Dispatchers.IO) {
@@ -184,12 +187,4 @@ class LocalStorageManager @Inject constructor(
CACHE_SIZE_MIN
}
}
private fun File.isReadable() = runCatching {
canRead()
}.getOrDefault(false)
private fun File.isWriteable() = runCatching {
canWrite()
}.getOrDefault(false)
}

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.isWriteable
import org.koitharu.kotatsu.local.data.LocalStorageManager
import javax.inject.Inject
@@ -41,7 +42,7 @@ class MangaDirectorySelectViewModel @Inject constructor(
val dir = requireNotNull(storageManager.resolveUri(uri)) {
"Cannot resolve file name of \"$uri\""
}
if (!dir.canWrite()) {
if (!dir.isWriteable()) {
throw AccessDeniedException(dir)
}
if (dir !in storageManager.getApplicationStorageDirs()) {

View File

@@ -8,6 +8,8 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.isReadable
import org.koitharu.kotatsu.core.util.ext.isWriteable
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import java.io.File
@@ -69,7 +71,7 @@ class MangaDirectoriesViewModel @Inject constructor(
titleRes = 0,
file = dir,
isChecked = dir == downloadDir,
isAvailable = dir.canRead() && dir.canWrite(),
isAvailable = dir.isReadable() && dir.isWriteable(),
isRemovable = false,
)
}
@@ -79,7 +81,7 @@ class MangaDirectoriesViewModel @Inject constructor(
titleRes = 0,
file = dir,
isChecked = dir == downloadDir,
isAvailable = dir.canRead() && dir.canWrite(),
isAvailable = dir.isReadable() && dir.isWriteable(),
isRemovable = true,
)
}

View File

@@ -818,4 +818,5 @@
<string name="link_to_manga_in_app">Link to manga in Kotatsu</string>
<string name="clear_browser_data">Clear browser data</string>
<string name="clear_browser_data_summary">Clear browser data such as cache and cookies. Warning: Authorization in manga sources may become invalid</string>
<string name="no_write_permission_to_file">Does not have permission to write a file</string>
</resources>