From 5c0ab4b7a95575bf8e86ad3cd33ae48da767cba7 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Wed, 26 Jul 2023 14:44:07 -0300 Subject: [PATCH] api: Add dc_msg_save_file() which saves file copy at the provided path (#4309) ... and fails if file already exists. The UI should open the file saving dialog, defaulting to Downloads and original filename, when asked to save the file. After confirmation it should call dc_msg_save_file(). --- deltachat-ffi/deltachat.h | 13 +++++++++++++ deltachat-ffi/src/lib.rs | 28 ++++++++++++++++++++++++++++ deltachat-jsonrpc/src/api.rs | 29 ++++++++++++++++------------- src/blob.rs | 19 +++++++++++-------- src/imex/transfer.rs | 6 ++++++ src/message.rs | 14 ++++++++++++++ src/receive_imf/tests.rs | 6 +++++- 7 files changed, 93 insertions(+), 22 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 97fe035c3..da74dff3f 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4050,6 +4050,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 5d68a940d..b383675d1 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3362,6 +3362,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.rs b/deltachat-jsonrpc/src/api.rs index 3d4fbf422..c2af5f7e7 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::path::Path; use std::sync::Arc; use std::{collections::HashMap, str::FromStr}; @@ -1927,19 +1928,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 40072cb53..707cc3cb2 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1182,23 +1182,26 @@ 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_viewtype(), Viewtype::Image); 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 398fb1d9f..6c67822c0 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 90103cbd8..bb2024adb 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::blob::BlobObject; use crate::chat::{Chat, ChatId}; @@ -579,6 +580,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 a9c912714..5a678f42d 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -2921,11 +2921,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;