Improve FileNotFoundException handling (#1332)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user