Skip to content

Commit

Permalink
Corrupted tags storage notification (#341)
Browse files Browse the repository at this point in the history
* Stop reading on empty tags
* Fail if there is a line with empty tags in the middle
---------
Co-authored-by: Kirill Taran <[email protected]>
  • Loading branch information
shubertm authored Apr 18, 2023
1 parent 6721b8d commit 6864f89
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ class AggregatedTagsStorage(
shards.forEach {
it.remove(id)
}

override fun isCorrupted() = shards.any { it.isCorrupted() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import space.taran.arknavigator.utils.Converters.Companion.stringFromTags
import space.taran.arknavigator.utils.Converters.Companion.tagsFromString
import space.taran.arknavigator.utils.LogTags.TAGS_STORAGE
import space.taran.arknavigator.utils.Tags
import java.io.StreamCorruptedException
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
Expand All @@ -42,6 +43,8 @@ class PlainTagsStorage(

private lateinit var tagsById: MutableMap<ResourceId, Tags>

private var isCorrupted = false

suspend fun init() = withContext(Dispatchers.IO) {
val result = resources.map { it to NO_TAGS }
.toMap()
Expand All @@ -55,7 +58,9 @@ class PlainTagsStorage(
", last modified at $lastModified"
)

result.putAll(readStorage())
readStorage().onSuccess {
result.putAll(it)
}
} else {
Log.d(TAGS_STORAGE, "file $storageFile doesn't exist")
}
Expand Down Expand Up @@ -88,7 +93,9 @@ class PlainTagsStorage(
suspend fun readStorageIfChanged() {
if (storageFile.notExists()) return
if (lastModified != storageFile.getLastModifiedTime()) {
tagsById.putAll(readStorage())
readStorage().onSuccess {
tagsById.putAll(it)
}
}
}

Expand Down Expand Up @@ -128,11 +135,13 @@ class PlainTagsStorage(

override suspend fun cleanup(existing: Collection<ResourceId>) =
withContext(Dispatchers.IO) {
val disappeared = tagsById.keys.minus(existing)
if (!isCorrupted) {
val disappeared = tagsById.keys.minus(existing)

Log.d(TAGS_STORAGE, "forgetting ${disappeared.size} resources")
disappeared.forEach { tagsById.remove(it) }
persist()
Log.d(TAGS_STORAGE, "forgetting ${disappeared.size} resources")
disappeared.forEach { tagsById.remove(it) }
persist()
}
}

override suspend fun remove(id: ResourceId) = withContext(Dispatchers.IO) {
Expand All @@ -144,6 +153,8 @@ class PlainTagsStorage(
persist()
}

override fun isCorrupted() = isCorrupted

override fun setTags(id: ResourceId, tags: Tags) {
if (!tagsById.containsKey(id)) {
error("Storage isn't aware about this resource id")
Expand Down Expand Up @@ -190,68 +201,74 @@ class PlainTagsStorage(
// without this, we need to stop all competing devices to remove a tag or a resource
// so far, just ensuring that we are not losing additions

val outside = readStorage()
readStorage().onSuccess { outside ->

for (newId in outside.keys - tagsById.keys) {
Log.d(
TAGS_STORAGE,
"resource $newId got first tags from outside"
)
tagsById[newId] = outside[newId]!!
for (newId in outside.keys - tagsById.keys) {
Log.d(
TAGS_STORAGE,
"resource $newId got first tags from outside"
)
tagsById[newId] = outside[newId]!!
}

for (sharedId in outside.keys.intersect(tagsById.keys)) {
val theirs = outside[sharedId]
val ours = tagsById[sharedId]
if (theirs != ours) {
Log.d(
TAGS_STORAGE,
"resource $sharedId got new tags " +
"from outside: ${theirs!! - ours}"
)
tagsById[sharedId] = theirs.union(ours!!)
}
}
}

for (sharedId in outside.keys.intersect(tagsById.keys)) {
val theirs = outside[sharedId]
val ours = tagsById[sharedId]
if (theirs != ours) {
if (tagsById.isEmpty() || tagsById.all { it.value.isEmpty() }) {
if (exists) {
Log.d(
TAGS_STORAGE,
"resource $sharedId got new tags " +
"from outside: ${theirs!! - ours}"
"no tagged resources, deleting storage file"
)
tagsById[sharedId] = theirs.union(ours!!)
Files.delete(storageFile)
}
return@withContext
}
}

if (tagsById.isEmpty() || tagsById.all { it.value.isEmpty() }) {
if (exists) {
Log.d(TAGS_STORAGE, "no tagged resources, deleting storage file")
Files.delete(storageFile)
}
return@withContext
writeStorage()
}

writeStorage()
}

private suspend fun readStorage(): Map<ResourceId, Tags> =
private suspend fun readStorage(): Result<Map<ResourceId, Tags>> =
withContext(Dispatchers.IO) {
val lines = Files.readAllLines(storageFile, StandardCharsets.UTF_8)
verifyVersion(lines.removeAt(0))

val result = lines
.map {
try {
val tagsById = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
val id = ResourceId.fromString(parts[0])
val tags = tagsFromString(parts[1])
id to tags
}

if (tags.isEmpty()) {
throw AssertionError(
"Tags storage must not contain empty sets of tags"
)
}
if (tagsById.any { (id, tags) -> tags.isEmpty() }) {
isCorrupted = true
return@withContext Result.failure(StreamCorruptedException())
}

id to tags
if (tagsById.isEmpty()) {
isCorrupted = true
return@withContext Result.failure(IllegalArgumentException())
}
.toMap()

if (result.isEmpty()) {
throw AssertionError("Tags storage must not be empty")
Log.d(TAGS_STORAGE, "${tagsById.size} entries has been read")
return@withContext Result.success(tagsById.toMap())
} catch (e: Exception) {
isCorrupted = true
return@withContext Result.failure(e)
}

Log.d(TAGS_STORAGE, "${result.size} entries has been read")
return@withContext result
}

private suspend fun writeStorage() =
Expand Down Expand Up @@ -281,6 +298,8 @@ class PlainTagsStorage(
private const val STORAGE_VERSION = 2
private const val STORAGE_VERSION_PREFIX = "version "

const val TYPE = "Tags"

const val KEY_VALUE_SEPARATOR = ':'

private fun verifyVersion(header: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ interface TagsStorage {
suspend fun cleanup(existing: Collection<ResourceId>)

suspend fun remove(id: ResourceId)

fun isCorrupted(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import space.taran.arknavigator.mvp.model.repo.scores.ScoreStorage
import space.taran.arknavigator.mvp.model.repo.scores.ScoreStorageRepo
import space.taran.arknavigator.mvp.model.repo.stats.StatsStorage
import space.taran.arknavigator.mvp.model.repo.stats.StatsStorageRepo
import space.taran.arknavigator.mvp.model.repo.tags.PlainTagsStorage
import space.taran.arknavigator.mvp.model.repo.tags.TagsStorage
import space.taran.arknavigator.mvp.model.repo.tags.TagsStorageRepo
import space.taran.arknavigator.mvp.presenter.adapter.ResourceMetaDiffUtilCallback
Expand Down Expand Up @@ -133,6 +134,11 @@ class GalleryPresenter(
metadataStorage = metadataStorageRepo.provide(rootAndFav)
statsStorage = statsStorageRepo.provide(rootAndFav)
scoreStorage = scoreStorageRepo.provide(rootAndFav)

if (storage.isCorrupted()) viewState.showCorruptNotificationDialog(
PlainTagsStorage.TYPE
)

resources = resourcesIds.map { index.getMeta(it) }.toMutableList()
sortByScores = preferences.get(PreferenceKey.SortByScores)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import space.taran.arknavigator.mvp.model.repo.scores.ScoreStorage
import space.taran.arknavigator.mvp.model.repo.scores.ScoreStorageRepo
import space.taran.arknavigator.mvp.model.repo.stats.StatsStorage
import space.taran.arknavigator.mvp.model.repo.stats.StatsStorageRepo
import space.taran.arknavigator.mvp.model.repo.tags.PlainTagsStorage
import space.taran.arknavigator.mvp.model.repo.tags.TagsStorage
import space.taran.arknavigator.mvp.model.repo.tags.TagsStorageRepo
import space.taran.arknavigator.mvp.presenter.adapter.ResourcesGridPresenter
Expand Down Expand Up @@ -135,10 +136,16 @@ class ResourcesPresenter(
)
}
}.launchIn(presenterScope)
previewStorage = previewStorageRepo.provide(rootAndFav)
initIndexingListener()
index.reindex()
storage = tagsStorageRepo.provide(rootAndFav)
if (storage.isCorrupted()) {
viewState.showCorruptNotificationDialog(
PlainTagsStorage.TYPE
)
return@launch
}
previewStorage = previewStorageRepo.provide(rootAndFav)
initIndexingListener()
statsStorage = statsStorageRepo.provide(rootAndFav)
scoreStorage = scoreStorageRepo.provide(rootAndFav)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ interface GalleryView : CommonMvpView {

@StateStrategyType(SkipStrategy::class)
fun notifySelectedChanged(selected: List<ResourceId>)

@StateStrategyType(SkipStrategy::class)
fun showCorruptNotificationDialog(storageType: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ interface ResourcesView : CommonMvpView {
fun clearStackedToasts()
@StateStrategyType(SkipStrategy::class)
fun shareResources(resources: List<Path>)
@StateStrategyType(SkipStrategy::class)
fun showCorruptNotificationDialog(storageType: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ import space.taran.arknavigator.utils.Tag
import space.taran.arknavigator.utils.Tags
import space.taran.arknavigator.utils.Score
import space.taran.arklib.utils.extension
import space.taran.arknavigator.ui.fragments.dialog.StorageCorruptionNotificationDialogFragment
import space.taran.arknavigator.utils.LogTags.GALLERY_SCREEN
import space.taran.arknavigator.utils.extensions.makeGone
import space.taran.arknavigator.utils.extensions.makeVisible
import timber.log.Timber
import java.nio.file.Path
import kotlin.system.measureTimeMillis

class GalleryFragment : MvpAppCompatFragment(), GalleryView {

Expand Down Expand Up @@ -120,7 +122,10 @@ class GalleryFragment : MvpAppCompatFragment(), GalleryView {
fabStartSelect.isVisible = !selectingEnabled

removeResourceFab.setOnLongClickListener {
presenter.onRemoveFabClick()
val time = measureTimeMillis {
presenter.onRemoveFabClick()
}
Timber.tag(GALLERY_SCREEN).d("${time / 1000L}s")
true
}

Expand Down Expand Up @@ -258,6 +263,13 @@ class GalleryFragment : MvpAppCompatFragment(), GalleryView {
}
}

override fun showCorruptNotificationDialog(storageType: String) {
StorageCorruptionNotificationDialogFragment.newInstance(storageType).show(
childFragmentManager,
StorageCorruptionNotificationDialogFragment.TAG
)
}

override fun notifyResourcesChanged() {
setFragmentResult(REQUEST_RESOURCES_CHANGED_KEY, bundleOf())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import space.taran.arknavigator.ui.activity.MainActivity
import space.taran.arknavigator.ui.adapter.ResourcesRVAdapter
import space.taran.arknavigator.ui.adapter.TagsSelectorAdapter
import space.taran.arknavigator.ui.fragments.dialog.ConfirmationDialogFragment
import space.taran.arknavigator.ui.fragments.dialog.StorageCorruptionNotificationDialogFragment
import space.taran.arknavigator.ui.fragments.dialog.SortDialogFragment
import space.taran.arknavigator.ui.fragments.dialog.TagsSortDialogFragment
import space.taran.arknavigator.ui.fragments.utils.toast
Expand Down Expand Up @@ -309,6 +310,14 @@ class ResourcesFragment : MvpAppCompatFragment(), ResourcesView {
)
}

override fun showCorruptNotificationDialog(storageType: String) {
StorageCorruptionNotificationDialogFragment.newInstance(storageType)
.show(
childFragmentManager,
StorageCorruptionNotificationDialogFragment.TAG
)
}

override fun setPreviewGenerationProgress(isVisible: Boolean) {
binding.progressPreviewGeneration.isVisible = isVisible
}
Expand Down Expand Up @@ -447,6 +456,13 @@ class ResourcesFragment : MvpAppCompatFragment(), ResourcesView {
)
}
}

childFragmentManager.setFragmentResultListener(
StorageCorruptionNotificationDialogFragment.STORAGE_CORRUPTION_DETECTED,
this
) { _, _ ->
presenter.onBackClick()
}
}

private fun dragHandlerTouchListener(view: View, event: MotionEvent): Boolean {
Expand Down Expand Up @@ -550,6 +566,7 @@ class ResourcesFragment : MvpAppCompatFragment(), ResourcesView {
const val DELETE_CONFIRMATION_REQUEST_KEY = "deleteConfirm"
private const val MOVE_TO_PATH_KEY = "moveToPath"
const val RESET_SCORES_FOR_SELECTED = "resetSelectedScores"
const val STORAGE_CORRUPTION_DETECTED = "storage corrupted"

private const val DRAG_TRAVEL_TIME_THRESHOLD = 30 // milliseconds
private const val DRAG_TRAVEL_DELTA_THRESHOLD = 0.1 // ratio
Expand Down
Loading

0 comments on commit 6864f89

Please sign in to comment.