diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 1b1187bd7..987e0d3e4 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2659,14 +2659,11 @@ void dc_str_unref (char* str); * * @memberof dc_backup_sender_t * @param context The context. - * @param folder A Path to a temporary directory where the encrypted database - * export will be created. The directory is not automatically cleaned - * after the backup is sent. * @return Opaque object for sending the backup. * On errors, NULL is returned and dc_get_last_error()returns an error that * should be shown to the user. */ -dc_backup_provider_t* dc_provide_backup (dc_context_t* context, const char* folder); +dc_backup_provider_t* dc_provide_backup (dc_context_t* context); /** diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 4a04380e0..34a23b2f4 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4136,16 +4136,14 @@ pub type dc_backup_provider_t = BackupProvider; #[no_mangle] pub unsafe extern "C" fn dc_provide_backup( context: *mut dc_context_t, - folder: *const libc::c_char, ) -> *mut dc_backup_provider_t { if context.is_null() { eprintln!("ignoring careless call to dc_send_backup()"); return ptr::null_mut(); } let ctx = &*context; - let dir = as_path(folder); block_on(async move { - BackupProvider::prepare(ctx, dir) + BackupProvider::prepare(ctx) .await .map(|provider| Box::into_raw(Box::new(provider))) .log_err(ctx, "BackupProvider failed") diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index ddb4d193f..83e021afd 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::path::Path; use std::sync::Arc; use std::{collections::HashMap, str::FromStr}; @@ -1351,10 +1350,10 @@ impl CommandApi { /// This **stops IO**. After completion `start_io` must be called to restart IO. /// /// Returns the QR code as a rendered SVG image. - async fn provide_backup(&self, account_id: u32, path: String) -> Result { + async fn provide_backup(&self, account_id: u32) -> Result { let ctx = self.get_context(account_id).await?; ctx.stop_io().await; - let provider = imex::BackupProvider::prepare(&ctx, Path::new(&path)).await?; + let provider = imex::BackupProvider::prepare(&ctx).await?; let qr = provider.qr(); let svg = match generate_backup_qr(&ctx, &qr).await { Ok(svg) => svg, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index b606e33f4..607b495ec 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -129,17 +129,7 @@ impl From for QrObject { } Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint }, Qr::Account { domain } => QrObject::Account { domain }, - /// Provides a backup that can be retrieve. - /// - /// This contains all the data needed to connect to a device and download a - /// backup from it to configure the receiving device with the same account. Qr::Backup { ticket } => QrObject::Backup { - /// Printable version of the provider information. - /// - /// This is the printable version of a `sendme` ticket, which contains all - /// the information to connect to and authenticate a backup provider. - /// - /// The format is somewhat opaque, but `sendme` can deserialise this. ticket: ticket.to_string(), }, Qr::WebrtcInstance { diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 42c7b0595..5dd1b6783 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -489,11 +489,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu .await?; } "send-backup" => { - let tdir = tempfile::TempDir::new()?; - let dir = tdir.path(); - let provider = BackupProvider::prepare(&context, dir).await?; + let provider = BackupProvider::prepare(&context).await?; let qr = provider.qr(); let rendered = deltachat::qr_code_generator::generate_backup_qr(&context, &qr).await?; + let tdir = tempfile::TempDir::new()?; + let dir = tdir.path(); let file = dir.join("qr.svg"); tokio::fs::write(&file, rendered).await?; println!("The QR code is at: {}", file.display()); diff --git a/src/blob.rs b/src/blob.rs index b6920f6ce..ef2869bed 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -242,9 +242,7 @@ impl<'a> BlobObject<'a> { /// including the dot. E.g. "foo.txt" is returned as `("foo", /// ".txt")` while "bar" is returned as `("bar", "")`. /// - /// The extension part will always be lowercased. Note that [`crate::imex::get_backup`] - /// relies on this for safety, if uppercase extensions are ever allowed it needs to be - /// adapted. + /// The extension part will always be lowercased. fn sanitise_name(name: &str) -> (String, String) { let mut name = name.to_string(); for part in name.rsplit('/') { diff --git a/src/imex/transfer.rs b/src/imex/transfer.rs index 105468631..24e2f6fd7 100644 --- a/src/imex/transfer.rs +++ b/src/imex/transfer.rs @@ -22,7 +22,7 @@ //! getter can not connect to an impersonated provider and the provider does not offer the //! download to an impersonated getter. -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result}; use async_channel::Receiver; @@ -76,21 +76,25 @@ impl BackupProvider { /// the possible cancellation of the "ongoing process". /// /// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io - pub async fn prepare(context: &Context, dir: &Path) -> Result { - ensure!( - // TODO: Should we worry about path normalisation? - dir != context.get_blobdir(), - "Temporary database export directory should not be in blobdir" - ); + pub async fn prepare(context: &Context) -> Result { e2ee::ensure_secret_key_exists(context) .await .context("Private key not available, aborting backup export")?; // Acquire global "ongoing" mutex. let cancel_token = context.alloc_ongoing().await?; + let context_dir = context + .get_blobdir() + .parent() + .ok_or(anyhow!("Context dir not found"))?; + let dbfile = context_dir.join(DBFILE_BACKUP_NAME); + if fs::metadata(&dbfile).await.is_ok() { + fs::remove_file(&dbfile).await?; + warn!(context, "Previous database export deleted"); + } let res = tokio::select! { biased; - res = Self::prepare_inner(context, dir) => { + res = Self::prepare_inner(context, &dbfile) => { match res { Ok(slf) => Ok(slf), Err(err) => { @@ -112,6 +116,7 @@ impl BackupProvider { context.clone(), provider, cancel_token, + dbfile, )); Ok(Self { handle, ticket }) } @@ -119,11 +124,10 @@ impl BackupProvider { /// Creates the provider task. /// /// Having this as a function makes it easier to cancel it when needed. - async fn prepare_inner(context: &Context, dir: &Path) -> Result<(Provider, Ticket)> { + async fn prepare_inner(context: &Context, dbfile: &Path) -> Result<(Provider, Ticket)> { // Generate the token up front: we also use it to encrypt the database. let token = AuthToken::generate(); context.emit_event(SendProgress::Started.into()); - let dbfile = dir.join(DBFILE_BACKUP_NAME); export_database(context, &dbfile, token.to_string()) .await .context("Database export failed")?; @@ -131,7 +135,7 @@ impl BackupProvider { // Now we can be sure IO is not running. let mut files = vec![DataSource::with_name( - dbfile, + dbfile.to_owned(), format!("db/{DBFILE_BACKUP_NAME}"), )]; let blobdir = BlobDirContents::new(context).await?; @@ -165,6 +169,7 @@ impl BackupProvider { context: Context, mut provider: Provider, cancel_token: Receiver<()>, + dbfile: PathBuf, ) -> Result<()> { context.emit_event(SendProgress::ProviderListening.into()); let mut events = provider.subscribe(); @@ -213,7 +218,9 @@ impl BackupProvider { }, } }; - // TODO: delete the database? + if let Err(err) = fs::remove_file(&dbfile).await { + error!(context, "Failed to remove database export: {err:#}"); + } context.emit_event(SendProgress::Completed.into()); context.free_ongoing().await; res @@ -361,12 +368,16 @@ async fn on_blob( ) -> Result { ensure!(!name.is_empty(), "Received a nameless blob"); let path = if name.starts_with("db/") { - // We can only safely write to the blobdir. But the blobdir could have a file named - // exactly like our special name. We solve this by using an uppercase extension - // which is forbidden for normal blobs. - context + let context_dir = context .get_blobdir() - .join(format!("{DBFILE_BACKUP_NAME}.SPECIAL")) + .parent() + .ok_or(anyhow!("Context dir not found"))?; + let dbfile = context_dir.join(DBFILE_BACKUP_NAME); + if fs::metadata(&dbfile).await.is_ok() { + fs::remove_file(&dbfile).await?; + warn!(context, "Previous database export deleted"); + } + dbfile } else { ensure!(name.starts_with("blob/"), "malformatted blob name"); let blobname = name.rsplit('/').next().context("malformatted blob name")?; @@ -456,8 +467,6 @@ impl From for EventType { mod tests { use std::time::Duration; - use testdir::testdir; - use crate::chat::{get_chat_msgs, send_msg, ChatItem}; use crate::message::{Message, Viewtype}; use crate::test_utils::TestContextManager; @@ -466,7 +475,6 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_receive() { - let dir = testdir!(); let mut tcm = TestContextManager::new(); // Create first device. @@ -479,7 +487,7 @@ mod tests { send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap(); // Prepare to transfer backup. - let provider = BackupProvider::prepare(&ctx0, &dir).await.unwrap(); + let provider = BackupProvider::prepare(&ctx0).await.unwrap(); // Set up second device. let ctx1 = tcm.unconfigured().await; diff --git a/src/qr.rs b/src/qr.rs index a59036eba..e3625b8e9 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -66,7 +66,17 @@ pub enum Qr { Account { domain: String, }, + /// Provides a backup that can be retrieve. + /// + /// This contains all the data needed to connect to a device and download a backup from + /// it to configure the receiving device with the same account. Backup { + /// Printable version of the provider information. + /// + /// This is the printable version of a `sendme` ticket, which contains all the + /// information to connect to and authenticate a backup provider. + /// + /// The format is somewhat opaque, but `sendme` can deserialise this. ticket: sendme::provider::Ticket, }, WebrtcInstance { diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index 6aba36f6c..3f4b2dba2 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -322,7 +322,7 @@ mod tests { let dir = testdir!(); let mut tcm = TestContextManager::new(); let ctx = tcm.alice().await; - let provider = BackupProvider::prepare(&ctx, &dir).await.unwrap(); + let provider = BackupProvider::prepare(&ctx).await.unwrap(); let qr = provider.qr(); println!("{}", format_backup(&qr).unwrap());