mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 10:43:05 +00:00
android, desktop: storage breakdown in developer options (#7603)
* android, desktop: storage breakdown in developer options Port of the iOS Storage screen (#5529): lists each top-level entry of the app's data, config and temp folders with its recursive size, so it is possible to see what takes space. * android, desktop: simplify storage view Use DefaultProgressView, drop the bold total and redundant absoluteFile, and do not follow top-level symlinks (matches iOS; avoids counting the Android lib link). * android, desktop: storage view: do not crash on names the JVM cannot convert to Path Under a non-UTF-8 locale on Linux, File.toPath() throws InvalidPathException for a non-ASCII top-level name (e.g. a received file decrypted into tmpDir); log it and show 0 bytes. Also correct plan wording. * android, desktop: storage view: fold path conversion into treeSize, use produceState Also correct plan wording (du per-directory overhead, unlistable roots, Windows junctions). * android, desktop: storage view: open as card screen like other settings screens * android, desktop: storage view: list top-level entries as Path to keep their exact names File.listFiles + toPath loses names that are not valid in the JVM file-name encoding (non-ASCII under a C locale, invalid UTF-8), so those entries showed 0 bytes. Files.newDirectoryStream keeps the raw bytes. * android, desktop: storage view: normalize root paths before dropping nested roots A root spelled with .. (e.g. XDG_DATA_HOME=/x/data/../tmp) compared as nested in another root and its size was shown nowhere. * android, desktop: document storage view in spec and product docs * android, desktop: storage view: single catch when listing a root * android, desktop: do not log file paths in storage view The walk's IOException messages contain the full path, which in tmpDir can be a received file's original name. --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
1 parent
d72f68e6dc
commit
afcaf22100
7 files changed
+159
No files matched your search
+1
@@ -60,6 +60,7 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
|
||||
}
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls)
|
||||
SettingsActionItem(painterResource(MR.images.ic_folder_open), stringResource(MR.strings.storage), { ModalManager.start.showModal(cardScreen = true) { StorageView() } })
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.*
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
private class EntryUsage(val name: String, val bytes: Long)
|
||||
|
||||
private class RootUsage(val dir: File, val entries: List<EntryUsage>) {
|
||||
val bytes: Long = entries.sumOf { it.bytes }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StorageView() {
|
||||
val usage by produceState<List<RootUsage>?>(initialValue = null) {
|
||||
value = withContext(Dispatchers.IO) { storageRoots().map(::rootUsage) }
|
||||
}
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.storage))
|
||||
val roots = usage
|
||||
if (roots == null) {
|
||||
DefaultProgressView(null)
|
||||
} else {
|
||||
roots.forEachIndexed { i, root ->
|
||||
if (i > 0) SectionDividerSpaced()
|
||||
SectionView(root.dir.path) {
|
||||
UsageRow(stringResource(MR.strings.storage_total), root.bytes)
|
||||
root.entries.forEach { UsageRow(it.name, it.bytes) }
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UsageRow(name: String, bytes: Long) {
|
||||
SectionItemViewSpaceBetween {
|
||||
Text(name, Modifier.weight(1f))
|
||||
Text(formatBytes(bytes), Modifier.padding(start = DEFAULT_PADDING), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storageRoots(): List<File> {
|
||||
val dirs = listOf(dataDir, preferencesDir, tmpDir).map { it.normalize() }.distinct()
|
||||
return dirs.filter { dir -> dirs.none { it != dir && dir.startsWith(it) } }
|
||||
}
|
||||
|
||||
private fun rootUsage(root: File): RootUsage =
|
||||
RootUsage(
|
||||
root,
|
||||
rootEntries(root)
|
||||
.map { EntryUsage(it.fileName.toString(), treeSize(it)) }
|
||||
.sortedByDescending { it.bytes }
|
||||
)
|
||||
|
||||
private fun rootEntries(root: File): List<Path> =
|
||||
try {
|
||||
Files.newDirectoryStream(root.toPath()).use { it.toList() }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "StorageView rootEntries: ${e.javaClass.simpleName}")
|
||||
emptyList()
|
||||
}
|
||||
|
||||
private fun treeSize(path: Path): Long {
|
||||
var bytes = 0L
|
||||
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
bytes += attrs.size()
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
|
||||
Log.e(TAG, "StorageView visitFileFailed: ${exc.javaClass.simpleName}")
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
|
||||
if (exc != null) Log.e(TAG, "StorageView postVisitDirectory: ${exc.javaClass.simpleName}")
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
return bytes
|
||||
}
|
||||
@@ -1160,6 +1160,8 @@
|
||||
<string name="deprecated_options_section">Deprecated options</string>
|
||||
<string name="show_internal_errors">Show internal errors</string>
|
||||
<string name="show_slow_api_calls">Show slow API calls</string>
|
||||
<string name="storage">Storage</string>
|
||||
<string name="storage_total">Total</string>
|
||||
<string name="shutdown_alert_question">Shutdown?</string>
|
||||
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
|
||||
<string name="prefs_error_saving_settings">Error saving settings</string>
|
||||
|
||||
Reference in new issue
Block a user