Remove the need for a directory for db export

Plus on import use the context directory.  We can actually write there
just fine.
This commit is contained in:
Floris Bruynooghe
2023-02-16 16:06:41 +01:00
parent dcce6ef50b
commit 490a14c5ef
9 changed files with 48 additions and 48 deletions

View File

@@ -2659,14 +2659,11 @@ void dc_str_unref (char* str);
* *
* @memberof dc_backup_sender_t * @memberof dc_backup_sender_t
* @param context The context. * @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. * @return Opaque object for sending the backup.
* On errors, NULL is returned and dc_get_last_error()returns an error that * On errors, NULL is returned and dc_get_last_error()returns an error that
* should be shown to the user. * 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);
/** /**

View File

@@ -4136,16 +4136,14 @@ pub type dc_backup_provider_t = BackupProvider;
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn dc_provide_backup( pub unsafe extern "C" fn dc_provide_backup(
context: *mut dc_context_t, context: *mut dc_context_t,
folder: *const libc::c_char,
) -> *mut dc_backup_provider_t { ) -> *mut dc_backup_provider_t {
if context.is_null() { if context.is_null() {
eprintln!("ignoring careless call to dc_send_backup()"); eprintln!("ignoring careless call to dc_send_backup()");
return ptr::null_mut(); return ptr::null_mut();
} }
let ctx = &*context; let ctx = &*context;
let dir = as_path(folder);
block_on(async move { block_on(async move {
BackupProvider::prepare(ctx, dir) BackupProvider::prepare(ctx)
.await .await
.map(|provider| Box::into_raw(Box::new(provider))) .map(|provider| Box::into_raw(Box::new(provider)))
.log_err(ctx, "BackupProvider failed") .log_err(ctx, "BackupProvider failed")

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::{collections::HashMap, str::FromStr}; 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. /// This **stops IO**. After completion `start_io` must be called to restart IO.
/// ///
/// Returns the QR code as a rendered SVG image. /// Returns the QR code as a rendered SVG image.
async fn provide_backup(&self, account_id: u32, path: String) -> Result<String> { async fn provide_backup(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;
ctx.stop_io().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 qr = provider.qr();
let svg = match generate_backup_qr(&ctx, &qr).await { let svg = match generate_backup_qr(&ctx, &qr).await {
Ok(svg) => svg, Ok(svg) => svg,

View File

@@ -129,17 +129,7 @@ impl From<Qr> for QrObject {
} }
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint }, Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain }, 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 { 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(), ticket: ticket.to_string(),
}, },
Qr::WebrtcInstance { Qr::WebrtcInstance {

View File

@@ -489,11 +489,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.await?; .await?;
} }
"send-backup" => { "send-backup" => {
let tdir = tempfile::TempDir::new()?; let provider = BackupProvider::prepare(&context).await?;
let dir = tdir.path();
let provider = BackupProvider::prepare(&context, dir).await?;
let qr = provider.qr(); let qr = provider.qr();
let rendered = deltachat::qr_code_generator::generate_backup_qr(&context, &qr).await?; 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"); let file = dir.join("qr.svg");
tokio::fs::write(&file, rendered).await?; tokio::fs::write(&file, rendered).await?;
println!("The QR code is at: {}", file.display()); println!("The QR code is at: {}", file.display());

View File

@@ -242,9 +242,7 @@ impl<'a> BlobObject<'a> {
/// including the dot. E.g. "foo.txt" is returned as `("foo", /// including the dot. E.g. "foo.txt" is returned as `("foo",
/// ".txt")` while "bar" is returned as `("bar", "")`. /// ".txt")` while "bar" is returned as `("bar", "")`.
/// ///
/// The extension part will always be lowercased. Note that [`crate::imex::get_backup`] /// The extension part will always be lowercased.
/// relies on this for safety, if uppercase extensions are ever allowed it needs to be
/// adapted.
fn sanitise_name(name: &str) -> (String, String) { fn sanitise_name(name: &str) -> (String, String) {
let mut name = name.to_string(); let mut name = name.to_string();
for part in name.rsplit('/') { for part in name.rsplit('/') {

View File

@@ -22,7 +22,7 @@
//! getter can not connect to an impersonated provider and the provider does not offer the //! getter can not connect to an impersonated provider and the provider does not offer the
//! download to an impersonated getter. //! 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 anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use async_channel::Receiver; use async_channel::Receiver;
@@ -76,21 +76,25 @@ impl BackupProvider {
/// the possible cancellation of the "ongoing process". /// the possible cancellation of the "ongoing process".
/// ///
/// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io /// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
pub async fn prepare(context: &Context, dir: &Path) -> Result<Self> { pub async fn prepare(context: &Context) -> Result<Self> {
ensure!(
// TODO: Should we worry about path normalisation?
dir != context.get_blobdir(),
"Temporary database export directory should not be in blobdir"
);
e2ee::ensure_secret_key_exists(context) e2ee::ensure_secret_key_exists(context)
.await .await
.context("Private key not available, aborting backup export")?; .context("Private key not available, aborting backup export")?;
// Acquire global "ongoing" mutex. // Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?; 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! { let res = tokio::select! {
biased; biased;
res = Self::prepare_inner(context, dir) => { res = Self::prepare_inner(context, &dbfile) => {
match res { match res {
Ok(slf) => Ok(slf), Ok(slf) => Ok(slf),
Err(err) => { Err(err) => {
@@ -112,6 +116,7 @@ impl BackupProvider {
context.clone(), context.clone(),
provider, provider,
cancel_token, cancel_token,
dbfile,
)); ));
Ok(Self { handle, ticket }) Ok(Self { handle, ticket })
} }
@@ -119,11 +124,10 @@ impl BackupProvider {
/// Creates the provider task. /// Creates the provider task.
/// ///
/// Having this as a function makes it easier to cancel it when needed. /// 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. // Generate the token up front: we also use it to encrypt the database.
let token = AuthToken::generate(); let token = AuthToken::generate();
context.emit_event(SendProgress::Started.into()); context.emit_event(SendProgress::Started.into());
let dbfile = dir.join(DBFILE_BACKUP_NAME);
export_database(context, &dbfile, token.to_string()) export_database(context, &dbfile, token.to_string())
.await .await
.context("Database export failed")?; .context("Database export failed")?;
@@ -131,7 +135,7 @@ impl BackupProvider {
// Now we can be sure IO is not running. // Now we can be sure IO is not running.
let mut files = vec![DataSource::with_name( let mut files = vec![DataSource::with_name(
dbfile, dbfile.to_owned(),
format!("db/{DBFILE_BACKUP_NAME}"), format!("db/{DBFILE_BACKUP_NAME}"),
)]; )];
let blobdir = BlobDirContents::new(context).await?; let blobdir = BlobDirContents::new(context).await?;
@@ -165,6 +169,7 @@ impl BackupProvider {
context: Context, context: Context,
mut provider: Provider, mut provider: Provider,
cancel_token: Receiver<()>, cancel_token: Receiver<()>,
dbfile: PathBuf,
) -> Result<()> { ) -> Result<()> {
context.emit_event(SendProgress::ProviderListening.into()); context.emit_event(SendProgress::ProviderListening.into());
let mut events = provider.subscribe(); 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.emit_event(SendProgress::Completed.into());
context.free_ongoing().await; context.free_ongoing().await;
res res
@@ -361,12 +368,16 @@ async fn on_blob(
) -> Result<DataStream> { ) -> Result<DataStream> {
ensure!(!name.is_empty(), "Received a nameless blob"); ensure!(!name.is_empty(), "Received a nameless blob");
let path = if name.starts_with("db/") { let path = if name.starts_with("db/") {
// We can only safely write to the blobdir. But the blobdir could have a file named let context_dir = context
// exactly like our special name. We solve this by using an uppercase extension
// which is forbidden for normal blobs.
context
.get_blobdir() .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 { } else {
ensure!(name.starts_with("blob/"), "malformatted blob name"); ensure!(name.starts_with("blob/"), "malformatted blob name");
let blobname = name.rsplit('/').next().context("malformatted blob name")?; let blobname = name.rsplit('/').next().context("malformatted blob name")?;
@@ -456,8 +467,6 @@ impl From<ReceiveProgress> for EventType {
mod tests { mod tests {
use std::time::Duration; use std::time::Duration;
use testdir::testdir;
use crate::chat::{get_chat_msgs, send_msg, ChatItem}; use crate::chat::{get_chat_msgs, send_msg, ChatItem};
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
use crate::test_utils::TestContextManager; use crate::test_utils::TestContextManager;
@@ -466,7 +475,6 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_receive() { async fn test_send_receive() {
let dir = testdir!();
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
// Create first device. // Create first device.
@@ -479,7 +487,7 @@ mod tests {
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap(); send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
// Prepare to transfer backup. // Prepare to transfer backup.
let provider = BackupProvider::prepare(&ctx0, &dir).await.unwrap(); let provider = BackupProvider::prepare(&ctx0).await.unwrap();
// Set up second device. // Set up second device.
let ctx1 = tcm.unconfigured().await; let ctx1 = tcm.unconfigured().await;

View File

@@ -66,7 +66,17 @@ pub enum Qr {
Account { Account {
domain: String, 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 { 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, ticket: sendme::provider::Ticket,
}, },
WebrtcInstance { WebrtcInstance {

View File

@@ -322,7 +322,7 @@ mod tests {
let dir = testdir!(); let dir = testdir!();
let mut tcm = TestContextManager::new(); let mut tcm = TestContextManager::new();
let ctx = tcm.alice().await; let ctx = tcm.alice().await;
let provider = BackupProvider::prepare(&ctx, &dir).await.unwrap(); let provider = BackupProvider::prepare(&ctx).await.unwrap();
let qr = provider.qr(); let qr = provider.qr();
println!("{}", format_backup(&qr).unwrap()); println!("{}", format_backup(&qr).unwrap());