diff --git a/app/src/main/java/com/owncloud/android/utils/FileUtil.kt b/app/src/main/java/com/owncloud/android/utils/FileUtil.kt index 125543a989bf..a2f41da7c939 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileUtil.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileUtil.kt @@ -6,6 +6,7 @@ */ package com.owncloud.android.utils +import android.os.Environment import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.owncloud.android.lib.common.utils.Log_OC @@ -15,11 +16,16 @@ import kotlinx.coroutines.withContext import java.io.File import java.io.IOException import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path import java.nio.file.attribute.BasicFileAttributes +import java.util.UUID import java.util.concurrent.TimeUnit object FileUtil { private const val TAG = "FileUtil" + private const val WRITE_PROBE_PREFIX = ".nc_write_probe_" + private const val WRITE_PROBE_FILE_NAME = "probe" @JvmStatic fun getCreationTimestamp(file: File): Long? { @@ -46,23 +52,87 @@ object FileUtil { } suspend fun isFolderWritable(folder: File?): Boolean = withContext(Dispatchers.IO) { - if (folder == null || !folder.isDirectory() || !folder.canWrite()) { + if (folder == null) { + Log_OC.w(TAG, "isFolderWritable: folder is null") + return@withContext false + } + + val directory = try { + folder.toPath() + } catch (e: InvalidPathException) { + Log_OC.w(TAG, "isFolderWritable: invalid path: ${folder.path}: $e") return@withContext false } return@withContext try { - val tempDir = File(folder, ".test_write_dir_${System.currentTimeMillis()}") - if (!tempDir.mkdir()) return@withContext false + isUsableDirectory(directory) && canCreateAndDeleteIn(directory) + } catch (e: Exception) { + Log_OC.e(TAG, "isFolderWritable: $e") + false + } + } - val tempFile = File(tempDir, "test_file") - val created = tempFile.createNewFile() + private fun isUsableDirectory(directory: Path): Boolean { + if (!Files.exists(directory)) { + Log_OC.w(TAG, "isFolderWritable: existence cannot be verified: $directory") + return false + } - if (created) tempFile.delete() - tempDir.delete() + if (!Files.isDirectory(directory)) { + Log_OC.w(TAG, "isFolderWritable: not a directory: $directory") + return false + } - created - } catch (_: Exception) { - false + val storageState = Environment.getExternalStorageState(directory.toFile()) + if (storageState != Environment.MEDIA_MOUNTED && storageState != Environment.MEDIA_UNKNOWN) { + Log_OC.w(TAG, "isFolderWritable: storage is $storageState: $directory") + return false + } + + if (!Files.isWritable(directory)) { + Log_OC.w(TAG, "isFolderWritable: no write access: $directory") + return false + } + + Log_OC.i(TAG, "isFolderWritable: usable directory") + + return true + } + + private fun canCreateAndDeleteIn(directory: Path): Boolean { + val probeDirectory = directory.resolve("$WRITE_PROBE_PREFIX${UUID.randomUUID()}") + val probeFile = probeDirectory.resolve(WRITE_PROBE_FILE_NAME) + + val writable = + runCatchingLogged("create directory", probeDirectory) { Files.createDirectory(probeDirectory) } && + runCatchingLogged("create file", probeFile) { Files.createFile(probeFile) } && + runCatchingLogged("delete file", probeFile) { Files.delete(probeFile) } && + runCatchingLogged("delete directory", probeDirectory) { Files.delete(probeDirectory) } + + if (writable) { + Log_OC.d(TAG, "isFolderWritable: writable: $directory") + } else { + removeLeftovers(probeFile, probeDirectory) } + + Log_OC.i(TAG, "isFolderWritable: writable: $writable") + + return writable + } + + private fun removeLeftovers(vararg paths: Path) { + paths.forEach { path -> + runCatchingLogged("delete if exists", path) { + Files.deleteIfExists(path) + } + } + } + + private inline fun runCatchingLogged(step: String, target: Path, action: () -> Unit): Boolean = try { + action() + true + } catch (e: Exception) { + Log_OC.e(TAG, "isFolderWritable: cannot $step $target: $e") + false } }