fix(#147, #411): review round 7 - stable copy ownership on provider renames and hand-offs, ledger writes tied to their folder, back up after settings change
This commit is contained in:
1 parent
bb075d052e
commit
8754f046f2
8 files changed
+60
-18
No files matched your search
@@ -174,7 +174,7 @@ class BackupWorker(
|
||||
val stamp = Clock.System.now()
|
||||
val liveKeys = all.map { it.id.toString() }.toSet()
|
||||
suspend fun record(key: String, files: List<String>) =
|
||||
prefs.updateAutoBackupLedger { withLedgerEntry(it, key, files, liveKeys) }
|
||||
prefs.updateAutoBackupLedger(folder.toString()) { withLedgerEntry(it, key, files, liveKeys) }
|
||||
var written = 0
|
||||
val landedLatest = mutableSetOf<String>()
|
||||
for (plan in plans) {
|
||||
@@ -203,23 +203,28 @@ class BackupWorker(
|
||||
}
|
||||
// Best-effort: the stable copy never holds up the dated backup,
|
||||
// its pruning or the run's status. An old copy goes only once the
|
||||
// new one has landed.
|
||||
// new one has landed, so a series is never left without one.
|
||||
try {
|
||||
val latest = plan.latestName ?: return@withContext
|
||||
val latestKey = latestLedgerKey(target.key)
|
||||
// A provider that renamed it leaves a copy to clean up next run.
|
||||
val landed = exporter.writeToFolder(folder, listing, latest, bytes)
|
||||
record(latestKey, plan.staleLatest + landed)
|
||||
// A provider that renamed it leaves a copy to clean up next
|
||||
// run; the intended name stays ours only if it already was.
|
||||
val ownsLatest = landed == latest || latest in listing.files ||
|
||||
latest in ledger[latestKey].orEmpty()
|
||||
val current = listOfNotNull(landed, latest.takeIf { ownsLatest })
|
||||
record(latestKey, plan.staleLatest + plan.handOff + current)
|
||||
if (landed == latest) landedLatest += target.key
|
||||
val staleLeft = exporter.deleteFromFolder(listing, plan.staleLatest)
|
||||
record(latestKey, plan.staleLatest.filter { it in staleLeft } + landed)
|
||||
record(latestKey, plan.staleLatest.filter { it in staleLeft } + plan.handOff + current)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Stable copy of one series failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A layout switch retires the other layout's copies once every new one is there.
|
||||
if (landedLatest.size == plans.size) {
|
||||
val stableCopies = plans.count { it.latestName != null }
|
||||
if (stableCopies > 0 && landedLatest.size == stableCopies) {
|
||||
replacedLatestCopies(targets, prefs.autoBackupLedger.first(), existing).forEach { (key, names) ->
|
||||
withContext(NonCancellable) {
|
||||
val left = exporter.deleteFromFolder(listing, names)
|
||||
|
||||
@@ -195,7 +195,6 @@ class SettingsPrefs @Inject constructor(
|
||||
val autoBackupSelection: Flow<BackupSelection> = prefs.snapshot { it.autoBackupSelection }
|
||||
val autoBackupCombined: Flow<Boolean> = prefs.snapshot { it.autoBackupCombined }
|
||||
val autoBackupKeep: Flow<Int> = prefs.flow(SettingsKeys.autoBackupKeep)
|
||||
val autoBackupLegacyInstall: Flow<Boolean> = prefs.snapshot { it.autoBackupLegacyInstall }
|
||||
val autoBackupLedger: Flow<BackupLedger> = prefs.flow(SettingsKeys.autoBackupLedger)
|
||||
val specialDatesEnabled: Flow<Boolean> = prefs.flow(SettingsKeys.specialDatesEnabled)
|
||||
val specialDatesTypes: Flow<Set<SpecialDateType>> = prefs.snapshot { it.specialDatesTypes }
|
||||
@@ -370,9 +369,17 @@ class SettingsPrefs @Inject constructor(
|
||||
|
||||
suspend fun setAutoBackupKeep(keep: Int) = prefs.set(SettingsKeys.autoBackupKeep, keep)
|
||||
|
||||
/** Atomic, so a run's ledger writes never undo one made meanwhile (pinning the layout). */
|
||||
suspend fun updateAutoBackupLedger(transform: (BackupLedger) -> BackupLedger) {
|
||||
prefs.update(SettingsKeys.autoBackupLedger, transform)
|
||||
/**
|
||||
* Atomic, so a run's ledger writes never undo one made meanwhile (pinning
|
||||
* the layout). Dropped when the folder is no longer [folder]: a run still
|
||||
* finishing must not carry the old folder's names into the new one's ledger.
|
||||
*/
|
||||
suspend fun updateAutoBackupLedger(folder: String, transform: (BackupLedger) -> BackupLedger) {
|
||||
prefs.edit {
|
||||
if (it[SettingsKeys.autoBackupFolderUri] == folder) {
|
||||
it[SettingsKeys.autoBackupLedger] = transform(it[SettingsKeys.autoBackupLedger])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Record a pre-#411 install's old file as its combined series' stable copy (see [pinBackupLayout]). */
|
||||
|
||||
@@ -76,6 +76,11 @@ data class BackupFilePlan(
|
||||
val latestName: String?,
|
||||
/** This series' earlier stable copies under a name it no longer uses (a rename), to delete. */
|
||||
val staleLatest: List<String> = emptyList(),
|
||||
/**
|
||||
* Stable copies this series held that another series of the run now takes
|
||||
* over: kept on record until that series has claimed them.
|
||||
*/
|
||||
val handOff: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -190,6 +195,8 @@ fun planBackupFiles(
|
||||
latestName = latest.takeIf { writable },
|
||||
staleLatest = ledger[latestLedgerKey(target.key)].orEmpty()
|
||||
.filter { it in existing && it != latest && it !in currentLatest },
|
||||
handOff = ledger[latestLedgerKey(target.key)].orEmpty()
|
||||
.filter { it in existing && it != latest && it in currentLatest },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,10 @@ fun BackupScreen(
|
||||
calendars = ready.exportable,
|
||||
selection = autoBackup.selection,
|
||||
onCheckedChange = viewModel::setAutoBackupCalendar,
|
||||
onDismiss = { showBackupCalendars = false },
|
||||
onDismiss = {
|
||||
showBackupCalendars = false
|
||||
viewModel.autoBackupCalendarsChosen()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +133,13 @@ class CalendarsViewModel @Inject constructor(
|
||||
_backupResult.value = try {
|
||||
val count = withContext(io) {
|
||||
val events = repository.exportEvents(calendarIds)
|
||||
// A one-calendar file is named after it, as the automatic backup's are.
|
||||
val name = calendarIds.singleOrNull()?.let { id ->
|
||||
repository.calendars().first().firstOrNull { it.id == id }?.displayName
|
||||
}
|
||||
icsExporter.writeDocument(
|
||||
uri = uri,
|
||||
content = IcsWriter().writeCalendar(events, Clock.System.now()),
|
||||
content = IcsWriter().writeCalendar(events, Clock.System.now(), name),
|
||||
)
|
||||
events.size
|
||||
}
|
||||
@@ -218,11 +222,28 @@ class CalendarsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun setAutoBackupCombined(combined: Boolean) {
|
||||
viewModelScope.launch { settingsPrefs.setAutoBackupCombined(combined) }
|
||||
viewModelScope.launch {
|
||||
settingsPrefs.setAutoBackupCombined(combined)
|
||||
runBackupIfActive()
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoBackupKeep(keep: Int) {
|
||||
viewModelScope.launch { settingsPrefs.setAutoBackupKeep(keep) }
|
||||
viewModelScope.launch {
|
||||
settingsPrefs.setAutoBackupKeep(keep)
|
||||
runBackupIfActive()
|
||||
}
|
||||
}
|
||||
|
||||
/** After the calendar choice is closed, back up what is now selected rather than at the next interval. */
|
||||
fun autoBackupCalendarsChosen() {
|
||||
viewModelScope.launch { runBackupIfActive() }
|
||||
}
|
||||
|
||||
private suspend fun runBackupIfActive() {
|
||||
if (settingsPrefs.autoBackupEnabled.first() && settingsPrefs.autoBackupFolderUri.first() != null) {
|
||||
BackupScheduler.runNow(context)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reschedule() {
|
||||
|
||||
@@ -726,13 +726,11 @@ class SettingsPrefsTest {
|
||||
}
|
||||
assertThat(prefs.autoBackupCombined.first()).isTrue()
|
||||
|
||||
assertThat(prefs.autoBackupLegacyInstall.first()).isTrue()
|
||||
|
||||
// Picking another folder pins the layout instead of flipping it, and
|
||||
// keeps the old file recognised as this install's own.
|
||||
prefs.setAutoBackupFolderUri("content://tree/new")
|
||||
assertThat(prefs.autoBackupCombined.first()).isTrue()
|
||||
assertThat(prefs.autoBackupLegacyInstall.first()).isFalse()
|
||||
// A new folder starts a new ledger: the old file there isn't known to be ours.
|
||||
assertThat(prefs.autoBackupLedger.first()).isEmpty()
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ class BackupPlanTest {
|
||||
keep = 1,
|
||||
)
|
||||
assertThat(plans[0].staleLatest).isEmpty()
|
||||
assertThat(plans[0].handOff).containsExactly("Work.ics")
|
||||
assertThat(plans[1].latestName).isEqualTo("Work.ics")
|
||||
}
|
||||
|
||||
|
||||
@@ -479,8 +479,8 @@ Each series also keeps a stable undated copy (`<calendar name>.ics`, or
|
||||
`calendula-backup.ics` combined) overwritten every run, so sync tools and
|
||||
subscriptions can follow one path. Retention never touches it, and it is only
|
||||
written over a file the series itself (or a series renaming away from that name
|
||||
in the same run) recorded. A renamed calendar's old copy is deleted, and so is a
|
||||
copy the layout switch replaced (per-calendar ↔ combined); a calendar taken out
|
||||
in the same run) recorded. A renamed calendar's old copy is deleted once the new
|
||||
one has landed, and so is a copy the layout switch replaced (per-calendar ↔ combined); a calendar taken out
|
||||
of the backup keeps its last one. The pre-#411 fixed `calendula-backup.ics` is
|
||||
the combined series' stable copy for the install that wrote it, and an
|
||||
install that already had a folder stays on the combined layout.
|
||||
|
||||
Reference in new issue
Block a user