diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 38b4b97c4c..f8453a0699 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4006,6 +4006,19 @@ char* dc_msg_get_subject (const dc_msg_t* msg); char* dc_msg_get_file (const dc_msg_t* msg); +/** + * Save file copy at the user-provided path. + * + * Fails if file already exists at the provided path. + * + * @memberof dc_msg_t + * @param msg The message object. + * @param path Destination file path with filename and extension. + * @return 0 on failure, 1 on success. + */ +int dc_msg_save_file (const dc_msg_t* msg, const char* path); + + /** * Get an original attachment filename, with extension but without the path. To get the full path, * use dc_msg_get_file(). diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 58d238fd82..6d08d9af6b 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3309,6 +3309,34 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha .unwrap_or_else(|| "".strdup()) } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_save_file( + msg: *mut dc_msg_t, + path: *const libc::c_char, +) -> libc::c_int { + if msg.is_null() || path.is_null() { + eprintln!("ignoring careless call to dc_msg_save_file()"); + return 0; + } + let ffi_msg = &*msg; + let ctx = &*ffi_msg.context; + let path = to_string_lossy(path); + let r = block_on( + ffi_msg + .message + .save_file(ctx, &std::path::PathBuf::from(path)), + ); + match r { + Ok(()) => 1, + Err(_) => { + r.context("Failed to save file from message") + .log_err(ctx) + .unwrap_or_default(); + 0 + } + } +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char { if msg.is_null() { diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 02027af0b8..f1b1b08c9c 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::path::Path; use std::sync::Arc; use std::{collections::HashMap, str::FromStr}; @@ -1894,19 +1895,21 @@ impl CommandApi { ); let destination_path = account_folder.join("stickers").join(collection); fs::create_dir_all(&destination_path).await?; - let file = message.get_file(&ctx).context("no file")?; - fs::copy( - &file, - destination_path.join(format!( - "{}.{}", - msg_id, - file.extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - )), - ) - .await?; + let file = message.get_filename().context("no file?")?; + message + .save_file( + &ctx, + &destination_path.join(format!( + "{}.{}", + msg_id, + Path::new(&file) + .extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + )), + ) + .await?; Ok(()) } diff --git a/src/blob.rs b/src/blob.rs index 579a2ad4e4..110b19139a 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1097,22 +1097,25 @@ mod tests { let alice_msg = alice.get_last_msg().await; assert_eq!(alice_msg.get_width() as u32, compressed_width); assert_eq!(alice_msg.get_height() as u32, compressed_height); - check_image_size( - alice_msg.get_file(&alice).unwrap(), - compressed_width, - compressed_height, - ); + let file_saved = alice + .get_blobdir() + .join("saved-".to_string() + &alice_msg.get_filename().unwrap()); + alice_msg.save_file(&alice, &file_saved).await?; + check_image_size(file_saved, compressed_width, compressed_height); let bob_msg = bob.recv_msg(&sent).await; assert_eq!(bob_msg.get_width() as u32, compressed_width); assert_eq!(bob_msg.get_height() as u32, compressed_height); - let file = bob_msg.get_file(&bob).unwrap(); + let file_saved = bob + .get_blobdir() + .join("saved-".to_string() + &bob_msg.get_filename().unwrap()); + bob_msg.save_file(&bob, &file_saved).await?; - let blob = BlobObject::new_from_path(&bob, &file).await?; + let blob = BlobObject::new_from_path(&bob, &file_saved).await?; let (_, exif) = blob.metadata()?; assert!(exif.is_none()); - let img = check_image_size(file, compressed_width, compressed_height); + let img = check_image_size(file_saved, compressed_width, compressed_height); Ok(img) } diff --git a/src/imex/transfer.rs b/src/imex/transfer.rs index 545bd94bd5..b158c80030 100644 --- a/src/imex/transfer.rs +++ b/src/imex/transfer.rs @@ -656,6 +656,12 @@ mod tests { let text = fs::read_to_string(&path).await.unwrap(); assert_eq!(text, "i am attachment"); + let path = path.with_file_name("saved.txt"); + msg.save_file(&ctx1, &path).await.unwrap(); + let text = fs::read_to_string(&path).await.unwrap(); + assert_eq!(text, "i am attachment"); + assert!(msg.save_file(&ctx1, &path).await.is_err()); + // Check that both received the ImexProgress events. ctx0.evtracker .get_matching(|ev| matches!(ev, EventType::ImexProgress(1000))) diff --git a/src/message.rs b/src/message.rs index d79208a8e8..6940c03c79 100644 --- a/src/message.rs +++ b/src/message.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::{ensure, format_err, Context as _, Result}; use deltachat_derive::{FromSql, ToSql}; use serde::{Deserialize, Serialize}; +use tokio::{fs, io}; use crate::chat::{Chat, ChatId}; use crate::config::Config; @@ -566,6 +567,19 @@ impl Message { self.param.get_path(Param::File, context).unwrap_or(None) } + /// Save file copy at the user-provided path. + pub async fn save_file(&self, context: &Context, path: &Path) -> Result<()> { + let path_src = self.get_file(context).context("No file")?; + let mut src = fs::OpenOptions::new().read(true).open(path_src).await?; + let mut dst = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .await?; + io::copy(&mut src, &mut dst).await?; + Ok(()) + } + /// If message is an image or gif, set Param::Width and Param::Height pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { if self.viewtype.has_file() { diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 5036b1a24a..d95f1659a2 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -2830,11 +2830,15 @@ async fn test_long_and_duplicated_filenames() -> Result<()> { let resulting_filename = msg.get_filename().unwrap(); assert_eq!(resulting_filename, filename); let path = msg.get_file(t).unwrap(); + let path2 = path.with_file_name("saved.txt"); + msg.save_file(t, &path2).await.unwrap(); assert!( path.to_str().unwrap().ends_with(".tar.gz"), "path {path:?} doesn't end with .tar.gz" ); - assert_eq!(fs::read_to_string(path).await.unwrap(), content); + assert_eq!(fs::read_to_string(&path).await.unwrap(), content); + assert_eq!(fs::read_to_string(&path2).await.unwrap(), content); + fs::remove_file(path2).await.unwrap(); } check_message(&msg_alice, &alice, filename_sent, &content).await; check_message(&msg_bob, &bob, filename_sent, &content).await;