feat(sync): добавлен механизм снятия блокировки синхронизации для хранилищ
This commit is contained in:
@@ -371,6 +371,14 @@ class EncryptedStorageAccessor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceClearSyncLock() {
|
||||
syncLockMutex.withLock {
|
||||
openWriteSystemFile(SYNC_LOCK_FILENAME).use { out ->
|
||||
out.write(ByteArray(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun appendSyncEntry(path: String, operation: StorageSyncOperation) {
|
||||
val cleanedPath = if (path.startsWith("/")) path else "/$path"
|
||||
val entries = readSyncJournal()
|
||||
|
||||
@@ -669,6 +669,27 @@ class LocalStorageAccessor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceClearSyncLock() = withContext(ioDispatcher) {
|
||||
val lockPath = systemLockPath()
|
||||
if (!Files.exists(lockPath)) {
|
||||
return@withContext
|
||||
}
|
||||
Files.createDirectories(lockPath.parent)
|
||||
FileChannel.open(
|
||||
lockPath,
|
||||
StandardOpenOption.READ,
|
||||
StandardOpenOption.WRITE,
|
||||
).use { channel ->
|
||||
val fileLock = channel.lock()
|
||||
try {
|
||||
channel.truncate(0)
|
||||
channel.force(true)
|
||||
} finally {
|
||||
fileLock.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun systemLockPath(): Path =
|
||||
_filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME).resolve(SYNC_LOCK_FILENAME)
|
||||
|
||||
|
||||
@@ -636,6 +636,14 @@ class YandexStorageAccessor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun forceClearSyncLock() = withContext(ioDispatcher) {
|
||||
syncLockMutex.withLock {
|
||||
openWriteSystemFile(SYNC_LOCK_FILENAME).use { out ->
|
||||
out.write(ByteArray(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun appendSyncEntry(path: String, operation: StorageSyncOperation) {
|
||||
val cleanedPath = if (path.startsWith("/")) path else "/$path"
|
||||
val entries = readSyncJournal()
|
||||
|
||||
@@ -59,4 +59,10 @@ interface IStorageAccessor {
|
||||
suspend fun readSyncLock(): StorageSyncLock?
|
||||
suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean
|
||||
suspend fun releaseSyncLock(holderId: String)
|
||||
|
||||
/**
|
||||
* Сбрасывает lock синхронизации без проверки [holderId] (снятие «залипшей» блокировки).
|
||||
* Не использовать в обычном цикле синка — только для ручного вмешательства.
|
||||
*/
|
||||
suspend fun forceClearSyncLock()
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -63,6 +64,8 @@ fun StorageTree(
|
||||
onDisableEncryption: (Tree<IStorageInfo>) -> Unit,
|
||||
getStatusText: (Tree<IStorageInfo>) -> String,
|
||||
isEncryptionOpened: (Tree<IStorageInfo>) -> Boolean,
|
||||
isStorageSyncLockHeld: suspend (IStorageInfo) -> Boolean,
|
||||
onClearStorageSyncLock: (IStorageInfo) -> Unit,
|
||||
) {
|
||||
val cur = tree.value
|
||||
val available by cur.isAvailable.collectAsStateWithLifecycle()
|
||||
@@ -125,6 +128,15 @@ fun StorageTree(
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var syncLockHeld by remember { mutableStateOf<Boolean?>(null) }
|
||||
LaunchedEffect(expanded, cur.uuid) {
|
||||
if (expanded) {
|
||||
syncLockHeld = null
|
||||
syncLockHeld = isStorageSyncLockHeld(cur)
|
||||
} else {
|
||||
syncLockHeld = null
|
||||
}
|
||||
}
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
var showRemoveConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showLockDialog by remember { mutableStateOf(false) }
|
||||
@@ -166,6 +178,25 @@ fun StorageTree(
|
||||
text = { Text(stringResource(R.string.encrypt)) }
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
enabled = syncLockHeld == true,
|
||||
onClick = {
|
||||
expanded = false
|
||||
if (syncLockHeld == true) {
|
||||
onClearStorageSyncLock(cur)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
when (syncLockHeld) {
|
||||
null -> stringResource(R.string.storage_sync_lock_checking)
|
||||
true -> stringResource(R.string.storage_sync_unlock_action)
|
||||
false -> stringResource(R.string.storage_sync_not_locked)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showRenameDialog) {
|
||||
@@ -295,7 +326,9 @@ fun StorageTree(
|
||||
onCloseEncrypted,
|
||||
onDisableEncryption,
|
||||
getStatusText,
|
||||
isEncryptionOpened
|
||||
isEncryptionOpened,
|
||||
isStorageSyncLockHeld,
|
||||
onClearStorageSyncLock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,4 +328,37 @@ abstract class AbstractVaultBrowserViewModel(
|
||||
val openedMap = getOpenedStoragesUseCase.openedStorages.value
|
||||
return openedMap.containsKey(storage.uuid)
|
||||
}
|
||||
|
||||
suspend fun isStorageSyncLockHeld(storage: IStorageInfo): Boolean {
|
||||
val s = storage as? IStorage ?: return false
|
||||
return try {
|
||||
s.accessor.readSyncLock() != null
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun clearStorageSyncLock(storage: IStorageInfo) {
|
||||
taskOrchestrator.enqueue(
|
||||
title = "Снятие блокировки синхронизации",
|
||||
dispatcher = Dispatchers.IO,
|
||||
work = { ctx ->
|
||||
try {
|
||||
val s = storage as? IStorage
|
||||
if (s == null) {
|
||||
ctx.log(TaskLogLevel.Error, "Некорректное хранилище")
|
||||
_messages.emit("Некорректное хранилище")
|
||||
return@enqueue
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Снимаю блокировку синхронизации…")
|
||||
s.accessor.forceClearSyncLock()
|
||||
ctx.log(TaskLogLevel.Info, "Блокировка синхронизации снята")
|
||||
_messages.emit("Блокировка синхронизации снята")
|
||||
} catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, e.message ?: "Не удалось снять блокировку")
|
||||
_messages.emit(e.message ?: "Не удалось снять блокировку синхронизации")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ fun VaultBrowserScreen(
|
||||
onDisableEncryption = { tree -> viewModel.disableEncryption(tree.value) },
|
||||
getStatusText = { tree -> viewModel.getStorageStatus(tree.value) },
|
||||
isEncryptionOpened = { tree -> viewModel.isEncryptionSessionOpen(tree.value) },
|
||||
isStorageSyncLockHeld = { info -> viewModel.isStorageSyncLockHeld(info) },
|
||||
onClearStorageSyncLock = { info -> viewModel.clearStorageSyncLock(info) },
|
||||
)
|
||||
}
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<string name="new_name_title">New name</string>
|
||||
<string name="remove_confirmation_dialog">Delete storage "%1$s"?</string>
|
||||
<string name="storage_lock_actions">Storage encryption actions</string>
|
||||
<string name="storage_sync_lock_checking">Проверка блокировки…</string>
|
||||
<string name="storage_sync_unlock_action">Снять блокировку синхронизации</string>
|
||||
<string name="storage_sync_not_locked">Синхронизация не заблокирована</string>
|
||||
|
||||
<string name="task_pipeline_title">Task pipeline</string>
|
||||
<string name="task_pipeline_jobs">Jobs</string>
|
||||
|
||||
@@ -27,8 +27,14 @@ class RunStorageSyncUseCase(
|
||||
syncEngine.syncAllGroups { fraction, label ->
|
||||
ctx.reportProgress(fraction, label)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, "Storage sync finished")
|
||||
ctx.reportProgress(null, "Storage sync: completed")
|
||||
} finally {
|
||||
}
|
||||
catch (e: Exception) {
|
||||
ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}")
|
||||
ctx.reportProgress(null, "Storage sync: failed - ${e.message}")
|
||||
}
|
||||
finally {
|
||||
running.set(false)
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user