core: add sqlite savepoints

axum-login-upgrade
Manos Pitsidianakis 2023-05-10 16:27:42 +03:00
parent 28156fdb75
commit 243f4af198
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
15 changed files with 325 additions and 54 deletions

View File

@ -30,6 +30,7 @@ use mailpot::{
melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
models::{changesets::*, *},
queue::{Queue, QueueEntry},
transaction::TransactionBehavior,
Configuration, Connection, Error, ErrorKind, Result, *,
};
use mailpot_cli::*;
@ -507,6 +508,7 @@ fn run_app(opt: Opt) -> Result<()> {
println!("post dry_run{:?}", dry_run);
}
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
match Envelope::from_bytes(input.as_bytes(), None) {
@ -514,7 +516,7 @@ fn run_app(opt: Opt) -> Result<()> {
if opt.debug {
eprintln!("{:?}", &env);
}
db.post(&env, input.as_bytes(), dry_run)?;
tx.post(&env, input.as_bytes(), dry_run)?;
}
Err(err) if input.trim().is_empty() => {
eprintln!("Empty input, abort.");
@ -522,21 +524,28 @@ fn run_app(opt: Opt) -> Result<()> {
}
Err(err) => {
eprintln!("Could not parse message: {}", err);
let p = db.conf().save_message(input)?;
let p = tx.conf().save_message(input)?;
eprintln!("Message saved at {}", p.display());
return Err(err.into());
}
}
tx.commit()?;
}
FlushQueue { dry_run } => {
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
let messages = if opt.debug {
println!("flush-queue dry_run {:?}", dry_run);
db.queue(Queue::Out)?
tx.queue(Queue::Out)?
.into_iter()
.map(DbVal::into_inner)
.chain(
tx.queue(Queue::Deferred)?
.into_iter()
.map(DbVal::into_inner),
)
.collect()
} else {
db.delete_from_queue(Queue::Out, vec![])?
tx.delete_from_queue(Queue::Out, vec![])?
};
if opt.verbose > 0 || opt.debug {
println!("Queue out has {} messages.", messages.len());
@ -544,7 +553,7 @@ fn run_app(opt: Opt) -> Result<()> {
let mut failures = Vec::with_capacity(messages.len());
let send_mail = db.conf().send_mail.clone();
let send_mail = tx.conf().send_mail.clone();
match send_mail {
mailpot::SendMail::ShellCommand(cmd) => {
fn submit(cmd: &str, msg: &QueueEntry) -> Result<()> {
@ -589,18 +598,27 @@ fn run_app(opt: Opt) -> Result<()> {
}
}
mailpot::SendMail::Smtp(_) => {
let conn_future = db.new_smtp_connection()?;
smol::future::block_on(smol::spawn(async move {
let conn_future = tx.new_smtp_connection()?;
failures = smol::future::block_on(smol::spawn(async move {
let mut conn = conn_future.await?;
for msg in messages {
if let Err(err) = Connection::submit(&mut conn, &msg, dry_run).await {
failures.push((err, msg));
}
}
Ok::<(), Error>(())
Ok::<_, Error>(failures)
}))?;
}
}
for (err, mut msg) in failures {
log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
msg.queue = Queue::Deferred;
tx.insert_to_queue(msg)?;
}
tx.commit()?;
}
ErrorQueue { cmd } => match cmd {
ErrorQueueCommand::List => {

View File

@ -136,7 +136,7 @@ fn test_out_queue_flush() {
log::info!("Subscribe two users, Αλίκη and Χαραλάμπης to foo-chat.");
{
let mut db = Connection::open_or_create_db(config.clone())
let db = Connection::open_or_create_db(config.clone())
.unwrap()
.trusted();
@ -204,7 +204,7 @@ fn test_out_queue_flush() {
);
{
let mut db = Connection::open_or_create_db(config.clone())
let db = Connection::open_or_create_db(config.clone())
.unwrap()
.trusted();
let mail = generate_mail("Χαραλάμπης", "", "hello world", "Hello there.", &mut seq);
@ -332,7 +332,7 @@ fn test_list_requests_submission() {
log::info!("User Αλίκη sends to foo-chat+request with subject 'help'.");
{
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
let db = Connection::open_or_create_db(config).unwrap().trusted();
let mail = generate_mail("Αλίκη", "+request", "help", "", &mut seq);
let subenvelope = mailpot::melib::Envelope::from_bytes(mail.as_bytes(), None)

View File

@ -17,7 +17,7 @@ error-chain = { version = "0.12.4", default-features = false }
log = "0.4"
melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms", "maildir_backend"], git = "https://github.com/meli/meli", rev = "2447a2c" }
minijinja = { version = "0.31.0", features = ["source", ] }
rusqlite = { version = "^0.28", features = ["bundled", "trace", "hooks", "serde_json", "array", "chrono"] }
rusqlite = { version = "^0.28", features = ["bundled", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] }
serde = { version = "^1", features = ["derive", ] }
serde_json = "^1"
toml = "^0.5"

View File

@ -199,7 +199,7 @@ impl Connection {
conn.busy_timeout(core::time::Duration::from_millis(500))?;
conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
let mut ret = Self {
let ret = Self {
conf,
connection: conn,
};
@ -232,13 +232,13 @@ impl Connection {
/// Migrate from version `from` to `to`.
///
/// See [Self::MIGRATIONS].
pub fn migrate(&mut self, mut from: u32, to: u32) -> Result<()> {
pub fn migrate(&self, mut from: u32, to: u32) -> Result<()> {
if from == to {
return Ok(());
}
let undo = from > to;
let tx = self.connection.transaction()?;
let tx = self.savepoint(Some(stringify!(migrate)))?;
while from != to {
log::trace!(
@ -247,15 +247,18 @@ impl Connection {
);
if undo {
trace!("{}", Self::MIGRATIONS[from as usize].2);
tx.execute(Self::MIGRATIONS[from as usize].2, [])?;
tx.connection
.execute(Self::MIGRATIONS[from as usize].2, [])?;
from -= 1;
} else {
trace!("{}", Self::MIGRATIONS[from as usize].1);
tx.execute(Self::MIGRATIONS[from as usize].1, [])?;
tx.connection
.execute(Self::MIGRATIONS[from as usize].1, [])?;
from += 1;
}
}
tx.pragma_update(None, "user_version", Self::MIGRATIONS[to as usize - 1].0)?;
tx.connection
.pragma_update(None, "user_version", Self::MIGRATIONS[to as usize - 1].0)?;
tx.commit()?;
@ -354,10 +357,10 @@ impl Connection {
}
/// Loads archive databases from [`Configuration::data_path`], if any.
pub fn load_archives(&mut self) -> Result<()> {
let tx = self.connection.transaction()?;
pub fn load_archives(&self) -> Result<()> {
let tx = self.savepoint(Some(stringify!(load_archives)))?;
{
let mut stmt = tx.prepare("ATTACH ? AS ?;")?;
let mut stmt = tx.connection.prepare("ATTACH ? AS ?;")?;
for archive in std::fs::read_dir(&self.conf.data_path)? {
let archive = archive?;
let path = archive.path();
@ -611,7 +614,7 @@ impl Connection {
}
/// Update a mailing list.
pub fn update_list(&mut self, change_set: MailingListChangeset) -> Result<()> {
pub fn update_list(&self, change_set: MailingListChangeset) -> Result<()> {
if matches!(
change_set,
MailingListChangeset {
@ -644,12 +647,12 @@ impl Connection {
hidden,
enabled,
} = change_set;
let tx = self.connection.transaction()?;
let tx = self.savepoint(Some(stringify!(update_list)))?;
macro_rules! update {
($field:tt) => {{
if let Some($field) = $field {
tx.execute(
tx.connection.execute(
concat!("UPDATE list SET ", stringify!($field), " = ? WHERE pk = ?;"),
rusqlite::params![&$field, &pk],
)?;
@ -673,7 +676,7 @@ impl Connection {
/// Execute operations inside an SQL transaction.
pub fn transaction(
&'_ self,
&'_ mut self,
behavior: transaction::TransactionBehavior,
) -> Result<transaction::Transaction<'_>> {
use transaction::*;
@ -689,6 +692,30 @@ impl Connection {
drop_behavior: DropBehavior::Rollback,
})
}
/// Execute operations inside an SQL savepoint.
pub fn savepoint(&'_ self, name: Option<&'static str>) -> Result<transaction::Savepoint<'_>> {
use std::sync::atomic::{AtomicUsize, Ordering};
use transaction::*;
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let name = name
.map(Ok)
.unwrap_or_else(|| Err(COUNTER.fetch_add(1, Ordering::Relaxed)));
match name {
Ok(ref n) => self.connection.execute_batch(&format!("SAVEPOINT {n}"))?,
Err(ref i) => self.connection.execute_batch(&format!("SAVEPOINT _{i}"))?,
};
Ok(Savepoint {
conn: self,
drop_behavior: DropBehavior::Rollback,
name,
committed: false,
})
}
}
/// Execute operations inside an SQL transaction.
@ -698,7 +725,7 @@ pub mod transaction {
/// A transaction handle.
#[derive(Debug)]
pub struct Transaction<'conn> {
pub(super) conn: &'conn Connection,
pub(super) conn: &'conn mut Connection,
pub(super) drop_behavior: DropBehavior,
}
@ -778,10 +805,10 @@ pub mod transaction {
/// DEFERRED means that the transaction does not actually start until
/// the database is first accessed.
Deferred,
#[default]
/// IMMEDIATE cause the database connection to start a new write
/// immediately, without waiting for a writes statement.
Immediate,
#[default]
/// EXCLUSIVE prevents other database connections from reading the
/// database while the transaction is underway.
Exclusive,
@ -806,6 +833,106 @@ pub mod transaction {
/// Panic. Used to enforce intentional behavior during development.
Panic,
}
/// A savepoint handle.
#[derive(Debug)]
pub struct Savepoint<'conn> {
pub(super) conn: &'conn Connection,
pub(super) drop_behavior: DropBehavior,
pub(super) name: std::result::Result<&'static str, usize>,
pub(super) committed: bool,
}
impl Drop for Savepoint<'_> {
fn drop(&mut self) {
_ = self.finish_();
}
}
impl Savepoint<'_> {
/// Commit and consume savepoint.
pub fn commit(mut self) -> Result<()> {
self.commit_()
}
fn commit_(&mut self) -> Result<()> {
if !self.committed {
match self.name {
Ok(ref n) => self
.conn
.connection
.execute_batch(&format!("RELEASE SAVEPOINT {n}"))?,
Err(ref i) => self
.conn
.connection
.execute_batch(&format!("RELEASE SAVEPOINT _{i}"))?,
};
self.committed = true;
}
Ok(())
}
/// Configure the savepoint to perform the specified action when it is
/// dropped.
#[inline]
pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
self.drop_behavior = drop_behavior;
}
/// A convenience method which consumes and rolls back a savepoint.
#[inline]
pub fn rollback(mut self) -> Result<()> {
self.rollback_()
}
fn rollback_(&mut self) -> Result<()> {
if !self.committed {
match self.name {
Ok(ref n) => self
.conn
.connection
.execute_batch(&format!("ROLLBACK TO SAVEPOINT {n}"))?,
Err(ref i) => self
.conn
.connection
.execute_batch(&format!("ROLLBACK TO SAVEPOINT _{i}"))?,
};
}
Ok(())
}
/// Consumes the savepoint, committing or rolling back according to
/// the current setting (see `drop_behavior`).
///
/// Functionally equivalent to the `Drop` implementation, but allows
/// callers to see any errors that occur.
#[inline]
pub fn finish(mut self) -> Result<()> {
self.finish_()
}
#[inline]
fn finish_(&mut self) -> Result<()> {
if self.conn.connection.is_autocommit() {
return Ok(());
}
match self.drop_behavior {
DropBehavior::Commit => self.commit_().or_else(|_| self.rollback_()),
DropBehavior::Rollback => self.rollback_(),
DropBehavior::Ignore => Ok(()),
DropBehavior::Panic => panic!("Savepoint dropped unexpectedly."),
}
}
}
impl std::ops::Deref for Savepoint<'_> {
type Target = Connection;
#[inline]
fn deref(&self) -> &Connection {
self.conn
}
}
}
#[cfg(test)]
@ -842,4 +969,124 @@ mod tests {
_ = Connection::open_or_create_db(config).unwrap();
}
#[test]
fn test_transactions() {
use melib::smtp::{SmtpAuth, SmtpSecurity, SmtpServerConf};
use tempfile::TempDir;
use super::transaction::*;
use crate::SendMail;
let tmp_dir = TempDir::new().unwrap();
let db_path = tmp_dir.path().join("mpot.db");
let data_path = tmp_dir.path().to_path_buf();
let config = Configuration {
send_mail: SendMail::Smtp(SmtpServerConf {
hostname: "127.0.0.1".into(),
port: 25,
envelope_from: "foo-chat@example.com".into(),
auth: SmtpAuth::None,
security: SmtpSecurity::None,
extensions: Default::default(),
}),
db_path,
data_path,
administrators: vec![],
};
let list = MailingList {
pk: 0,
name: "".into(),
id: "".into(),
description: None,
address: "".into(),
archive_url: None,
};
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
/* drop rollback */
let mut tx = db.transaction(Default::default()).unwrap();
tx.set_drop_behavior(DropBehavior::Rollback);
let _new = tx.create_list(list.clone()).unwrap();
drop(tx);
assert_eq!(&db.lists().unwrap(), &[]);
/* drop commit */
let mut tx = db.transaction(Default::default()).unwrap();
tx.set_drop_behavior(DropBehavior::Commit);
let new = tx.create_list(list.clone()).unwrap();
drop(tx);
assert_eq!(&db.lists().unwrap(), &[new.clone()]);
/* rollback with drop commit */
let mut tx = db.transaction(Default::default()).unwrap();
tx.set_drop_behavior(DropBehavior::Commit);
let _new2 = tx
.create_list(MailingList {
id: "1".into(),
address: "1".into(),
..list.clone()
})
.unwrap();
tx.rollback().unwrap();
assert_eq!(&db.lists().unwrap(), &[new.clone()]);
/* tx and then savepoint */
let tx = db.transaction(Default::default()).unwrap();
let sv = tx.savepoint(None).unwrap();
let new2 = sv
.create_list(MailingList {
id: "2".into(),
address: "2".into(),
..list.clone()
})
.unwrap();
sv.commit().unwrap();
tx.commit().unwrap();
assert_eq!(&db.lists().unwrap(), &[new.clone(), new2.clone()]);
/* tx and then rollback savepoint */
let tx = db.transaction(Default::default()).unwrap();
let sv = tx.savepoint(None).unwrap();
let _new3 = sv
.create_list(MailingList {
id: "3".into(),
address: "3".into(),
..list.clone()
})
.unwrap();
sv.rollback().unwrap();
tx.commit().unwrap();
assert_eq!(&db.lists().unwrap(), &[new.clone(), new2.clone()]);
/* tx, commit savepoint and then rollback commit */
let tx = db.transaction(Default::default()).unwrap();
let sv = tx.savepoint(None).unwrap();
let _new3 = sv
.create_list(MailingList {
id: "3".into(),
address: "3".into(),
..list.clone()
})
.unwrap();
sv.commit().unwrap();
tx.rollback().unwrap();
assert_eq!(&db.lists().unwrap(), &[new.clone(), new2.clone()]);
/* nested savepoints */
let tx = db.transaction(Default::default()).unwrap();
let sv = tx.savepoint(None).unwrap();
let sv1 = sv.savepoint(None).unwrap();
let new3 = sv1
.create_list(MailingList {
id: "3".into(),
address: "3".into(),
..list
})
.unwrap();
sv1.commit().unwrap();
sv.commit().unwrap();
tx.commit().unwrap();
assert_eq!(&db.lists().unwrap(), &[new, new2, new3]);
}
}

View File

@ -189,7 +189,7 @@ pub mod subscriptions;
mod templates;
pub use config::{Configuration, SendMail};
pub use connection::*;
pub use connection::{transaction, *};
pub use errors::*;
use models::*;
pub use templates::*;

View File

@ -84,7 +84,11 @@ impl Connection {
}
/// Process a new mailing list post.
pub fn post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
///
/// In case multiple processes can access the database at any time, use an
/// `EXCLUSIVE` transaction before calling this function.
/// See [`Connection::transaction`].
pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
let result = self.inner_post(env, raw, _dry_run);
if let Err(err) = result {
return match self.insert_to_queue(QueueEntry::new(
@ -115,7 +119,7 @@ impl Connection {
result
}
fn inner_post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
fn inner_post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
trace!("Received envelope to post: {:#?}", &env);
let tos = env.to().to_vec();
if tos.is_empty() {
@ -295,7 +299,7 @@ impl Connection {
/// Process a new mailing list request.
pub fn request(
&mut self,
&self,
list: &DbVal<MailingList>,
request: ListRequest,
env: &Envelope,

View File

@ -214,8 +214,8 @@ impl Connection {
}
/// Delete queue entries returning the deleted values.
pub fn delete_from_queue(&mut self, queue: Queue, index: Vec<i64>) -> Result<Vec<QueueEntry>> {
let tx = self.connection.transaction()?;
pub fn delete_from_queue(&self, queue: Queue, index: Vec<i64>) -> Result<Vec<QueueEntry>> {
let tx = self.savepoint(Some(stringify!(delete_from_queue)))?;
let cl = |row: &rusqlite::Row<'_>| {
Ok(QueueEntry {
@ -233,9 +233,11 @@ impl Connection {
})
};
let mut stmt = if index.is_empty() {
tx.prepare("DELETE FROM queue WHERE which = ? RETURNING *;")?
tx.connection
.prepare("DELETE FROM queue WHERE which = ? RETURNING *;")?
} else {
tx.prepare("DELETE FROM queue WHERE which = ? AND pk IN rarray(?) RETURNING *;")?
tx.connection
.prepare("DELETE FROM queue WHERE which = ? AND pk IN rarray(?) RETURNING *;")?
};
let iter = if index.is_empty() {
stmt.query_map([&queue.as_str()], cl)?
@ -279,7 +281,7 @@ mod tests {
administrators: vec![],
};
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
let db = Connection::open_or_create_db(config).unwrap().trusted();
for i in 0..5 {
db.insert_to_queue(
QueueEntry::new(

View File

@ -253,7 +253,7 @@ impl Connection {
}
/// Accept subscription candidate.
pub fn accept_candidate_subscription(&mut self, pk: i64) -> Result<DbVal<ListSubscription>> {
pub fn accept_candidate_subscription(&self, pk: i64) -> Result<DbVal<ListSubscription>> {
let val = self.connection.query_row(
"INSERT INTO subscription(list, address, name, enabled, digest, verified, \
hide_address, receive_duplicates, receive_own_posts, receive_confirmation) SELECT \
@ -311,7 +311,7 @@ impl Connection {
}
/// Update a mailing list subscription.
pub fn update_subscription(&mut self, change_set: ListSubscriptionChangeset) -> Result<()> {
pub fn update_subscription(&self, change_set: ListSubscriptionChangeset) -> Result<()> {
let pk = self
.list_subscription_by_address(change_set.list, &change_set.address)?
.pk;
@ -347,12 +347,12 @@ impl Connection {
receive_own_posts,
receive_confirmation,
} = change_set;
let tx = self.connection.transaction()?;
let tx = self.savepoint(Some(stringify!(update_subscription)))?;
macro_rules! update {
($field:tt) => {{
if let Some($field) = $field {
tx.execute(
tx.connection.execute(
concat!(
"UPDATE subscription SET ",
stringify!($field),
@ -547,7 +547,7 @@ impl Connection {
}
/// Update an account.
pub fn update_account(&mut self, change_set: AccountChangeset) -> Result<()> {
pub fn update_account(&self, change_set: AccountChangeset) -> Result<()> {
let Some(acc) = self.account_by_address(&change_set.address)? else {
return Err(NotFound("account with this address not found!").into());
};
@ -572,12 +572,12 @@ impl Connection {
password,
enabled,
} = change_set;
let tx = self.connection.transaction()?;
let tx = self.savepoint(Some(stringify!(update_account)))?;
macro_rules! update {
($field:tt) => {{
if let Some($field) = $field {
tx.execute(
tx.connection.execute(
concat!(
"UPDATE account SET ",
stringify!($field),
@ -616,7 +616,7 @@ mod tests {
administrators: vec![],
};
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
let db = Connection::open_or_create_db(config).unwrap().trusted();
let list = db
.create_list(MailingList {
pk: -1,

View File

@ -70,7 +70,7 @@ fn test_accounts() {
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 0);
let mut db = db.untrusted();
let db = db.untrusted();
let subscribe_bytes = b"From: Name <user@example.com>
To: <foo-chat+subscribe@example.com>

View File

@ -76,7 +76,7 @@ fn test_error_queue() {
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
// drop privileges
let mut db = db.untrusted();
let db = db.untrusted();
let input_bytes = include_bytes!("./test_sample_longmessage.eml");
let envelope = melib::Envelope::from_bytes(input_bytes, None).expect("Could not parse message");

View File

@ -34,7 +34,7 @@ fn test_init_empty() {
administrators: vec![],
};
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
let db = Connection::open_or_create_db(config).unwrap().trusted();
let migrations = Connection::MIGRATIONS;
if migrations.is_empty() {

View File

@ -39,7 +39,7 @@ fn test_smtp() {
administrators: vec![],
};
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.lists().unwrap().is_empty());
let foo_chat = db
.create_list(MailingList {
@ -193,7 +193,7 @@ fn test_smtp_mailcrab() {
administrators: vec![],
};
let mut db = Connection::open_or_create_db(config).unwrap().trusted();
let db = Connection::open_or_create_db(config).unwrap().trusted();
assert!(db.lists().unwrap().is_empty());
let foo_chat = db
.create_list(MailingList {

View File

@ -68,7 +68,7 @@ fn test_list_subscription() {
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 0);
let mut db = db.untrusted();
let db = db.untrusted();
let post_bytes = b"From: Name <user@example.com>
To: <foo-chat@example.com>
@ -193,7 +193,7 @@ fn test_post_rejection() {
assert_eq!(db.queue(Queue::Error).unwrap().len(), 0);
assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 0);
let mut db = db.untrusted();
let db = db.untrusted();
let post_bytes = b"From: Name <user@example.com>
To: <foo-chat@example.com>

View File

@ -346,7 +346,7 @@ pub async fn list_edit_post(
));
};
let mut db = db.trusted();
let db = db.trusted();
match payload {
ChangeSetting::PostPolicy {
delete_post_policy: _,

View File

@ -96,7 +96,7 @@ pub async fn settings_post(
Form(payload): Form<ChangeSetting>,
state: Arc<AppState>,
) -> Result<Redirect, ResponseError> {
let mut db = Connection::open_db(state.conf.clone())?;
let db = Connection::open_db(state.conf.clone())?;
let acc = db
.account_by_address(&user.address)
.with_status(StatusCode::BAD_REQUEST)?
@ -338,7 +338,7 @@ pub async fn user_list_subscription_post(
Form(payload): Form<SubscriptionFormPayload>,
state: Arc<AppState>,
) -> Result<Redirect, ResponseError> {
let mut db = Connection::open_db(state.conf.clone())?;
let db = Connection::open_db(state.conf.clone())?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id)?,