Refuse stores marked by a newer release (release/2026) #247
No files matched your search
@@ -106,6 +106,16 @@ Schema history:
|
||||
|
||||
pub const DATABASE_SCHEMA_VERSION: u32 = 6;
|
||||
|
||||
// inbuxa: the layout of the store as our own line keeps it, beside the
|
||||
// schema version above (which stays at 0.16's, so a Stalwart 0.16 store
|
||||
// still opens). Kept in the fork's subspace under `Vs`; a store without it
|
||||
// was last opened by a release from before the mark, or by Stalwart, and
|
||||
// gets it on start. A release refuses a store marked newer than its own.
|
||||
//
|
||||
// 1 - the 0.16 layout plus the fork's own subspace (2026.10.x, 27.1)
|
||||
pub const INBUXA_STORE_VERSION: u32 = 1;
|
||||
pub const INBUXA_STORE_VERSION_KEY: &[u8] = b"Vs";
|
||||
|
||||
pub const LONG_1D_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
pub const LONG_1Y_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24 * 365);
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
use crate::v016::migrate_v0_16;
|
||||
use common::{DATABASE_SCHEMA_VERSION, Server};
|
||||
use common::{DATABASE_SCHEMA_VERSION, INBUXA_STORE_VERSION, INBUXA_STORE_VERSION_KEY, Server};
|
||||
use store::{
|
||||
IterateParams, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REPORT_IN,
|
||||
IterateParams, SUBSPACE_INBUXA, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REPORT_IN,
|
||||
SUBSPACE_REPORT_OUT, SerializeInfallible,
|
||||
write::{AnyClass, AnyKey, BatchBuilder, ValueClass},
|
||||
};
|
||||
@@ -21,6 +21,15 @@ pub mod destroy;
|
||||
pub mod v016;
|
||||
|
||||
pub async fn try_migrate(server: &Server) -> trc::Result<()> {
|
||||
// inbuxa: a store a newer release has changed is refused before anything
|
||||
// below writes to it.
|
||||
let store_version = store_version(server).await?;
|
||||
if let Some(version) = store_version.filter(|version| *version > INBUXA_STORE_VERSION) {
|
||||
abort(&format!(
|
||||
"The data store was upgraded by a newer release (store version {version}, this release reads up to {INBUXA_STORE_VERSION}). Run that release again, or restore the backup taken before the upgrade, see https://docs.inbuxa.org/install/migrating/"
|
||||
));
|
||||
}
|
||||
|
||||
// inbuxa: before the version check, which returns early on a current
|
||||
// store, and before migrate_v0_16, which reads the renamed key.
|
||||
rename_spam_blobs(server).await?;
|
||||
@@ -36,7 +45,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
|
||||
{
|
||||
Some(DATABASE_SCHEMA_VERSION) => {
|
||||
if !std::env::var("DANGER_FORCE_MIGRATE").is_ok_and(|v| v == "1") {
|
||||
return Ok(());
|
||||
return mark_store(server, store_version).await;
|
||||
}
|
||||
}
|
||||
Some(0..=4) => {
|
||||
@@ -63,7 +72,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
|
||||
_ => {
|
||||
if is_new_install(server).await.caused_by(trc::location!())? {
|
||||
write_schema_version(server).await?;
|
||||
return Ok(());
|
||||
return mark_store(server, store_version).await;
|
||||
} else {
|
||||
abort(concat!(
|
||||
"You must first upgrade to version 0.15, please read ",
|
||||
@@ -74,7 +83,43 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
|
||||
}
|
||||
|
||||
migrate_v0_16(server).await?;
|
||||
write_schema_version(server).await
|
||||
write_schema_version(server).await?;
|
||||
mark_store(server, store_version).await
|
||||
}
|
||||
|
||||
async fn store_version(server: &Server) -> trc::Result<Option<u32>> {
|
||||
server
|
||||
.store()
|
||||
.get_value::<u32>(AnyKey {
|
||||
subspace: SUBSPACE_INBUXA,
|
||||
key: INBUXA_STORE_VERSION_KEY.to_vec(),
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
// inbuxa: written once, when a store has no mark yet
|
||||
async fn mark_store(server: &Server, store_version: Option<u32>) -> trc::Result<()> {
|
||||
if store_version.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.set(
|
||||
ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_INBUXA,
|
||||
key: INBUXA_STORE_VERSION_KEY.to_vec(),
|
||||
}),
|
||||
INBUXA_STORE_VERSION.serialize(),
|
||||
);
|
||||
|
||||
server
|
||||
.store()
|
||||
.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_schema_version(server: &Server) -> trc::Result<()> {
|
||||
|
||||
@@ -38,6 +38,8 @@ pub mod smtp;
|
||||
#[cfg(test)]
|
||||
pub mod store;
|
||||
#[cfg(test)]
|
||||
pub mod store_version; // inbuxa: INBUXA_STORE_VERSION
|
||||
#[cfg(test)]
|
||||
pub mod system;
|
||||
#[cfg(test)]
|
||||
pub mod telemetry;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! inbuxa: the store version mark. A store without it gets it on start, a
|
||||
//! marked store is left alone, and a store marked by a newer release is
|
||||
//! refused before anything is written to it.
|
||||
|
||||
use crate::utils::server::TestServerBuilder;
|
||||
use common::{INBUXA_STORE_VERSION, INBUXA_STORE_VERSION_KEY, Server};
|
||||
use futures::FutureExt;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use store::{
|
||||
SUBSPACE_INBUXA, SerializeInfallible,
|
||||
write::{AnyClass, AnyKey, BatchBuilder, ValueClass},
|
||||
};
|
||||
|
||||
async fn read_mark(server: &Server) -> Option<u32> {
|
||||
server
|
||||
.store()
|
||||
.get_value::<u32>(AnyKey {
|
||||
subspace: SUBSPACE_INBUXA,
|
||||
key: INBUXA_STORE_VERSION_KEY.to_vec(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn write_mark(server: &Server, version: Option<u32>) {
|
||||
let class = ValueClass::Any(AnyClass {
|
||||
subspace: SUBSPACE_INBUXA,
|
||||
key: INBUXA_STORE_VERSION_KEY.to_vec(),
|
||||
});
|
||||
let mut batch = BatchBuilder::new();
|
||||
match version {
|
||||
Some(version) => batch.set(class, version.serialize()),
|
||||
None => batch.clear(class),
|
||||
};
|
||||
server.store().write(batch.build_all()).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn store_version() {
|
||||
let test = TestServerBuilder::new("store_version")
|
||||
.await
|
||||
.disable_services()
|
||||
.build()
|
||||
.await;
|
||||
let server = &test.server;
|
||||
|
||||
println!("Running store version tests...");
|
||||
|
||||
// A store from before the mark (or from Stalwart) gets it on start.
|
||||
write_mark(server, None).await;
|
||||
migration::try_migrate(server).await.unwrap();
|
||||
assert_eq!(read_mark(server).await, Some(INBUXA_STORE_VERSION));
|
||||
|
||||
// Starting again leaves it as it is.
|
||||
migration::try_migrate(server).await.unwrap();
|
||||
assert_eq!(read_mark(server).await, Some(INBUXA_STORE_VERSION));
|
||||
|
||||
// A store marked by a newer release is refused, and the mark stays.
|
||||
let newer = INBUXA_STORE_VERSION + 1;
|
||||
write_mark(server, Some(newer)).await;
|
||||
let result = AssertUnwindSafe(migration::try_migrate(server))
|
||||
.catch_unwind()
|
||||
.await;
|
||||
assert!(result.is_err(), "a store marked newer must be refused");
|
||||
assert_eq!(read_mark(server).await, Some(newer));
|
||||
|
||||
// Put back, so the store is usable again.
|
||||
write_mark(server, Some(INBUXA_STORE_VERSION)).await;
|
||||
migration::try_migrate(server).await.unwrap();
|
||||
}
|
||||
Reference in new issue
Block a user