Improve FileNotFoundException handling (#1332)

master
Koitharu 1 year ago
parent f7c70577ae
commit 6a0ad7f79b
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

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

@ -41,6 +41,7 @@ import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import java.io.File
import java.net.ConnectException import java.net.ConnectException
import java.net.NoRouteToHostException import java.net.NoRouteToHostException
import java.net.SocketException 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 MSG_CONNECTION_RESET = "Connection reset"
private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported" 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) fun Throwable.getDisplayMessage(resources: Resources): String = getDisplayMessageOrNull(resources)
?: resources.getString(R.string.error_occurred) ?: 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 UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is BadBackupFormatException -> resources.getString(R.string.unsupported_backup_message) 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 AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration) is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration)
@ -225,3 +228,35 @@ fun Throwable.isWebViewUnavailable(): Boolean {
@Suppress("FunctionName") @Suppress("FunctionName")
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT) 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,
)
}
}

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

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

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

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

@ -818,4 +818,5 @@
<string name="link_to_manga_in_app">Link to manga in Kotatsu</string> <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">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="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> </resources>

Loading…
Cancel
Save