From 56b86adf18f3b3aed8037f57548ad0876255f5f8 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 | 38 ++++++++++++++++++++++++------------ src/blob.rs | 19 ++++++++++-------- src/imex/transfer.rs | 6 ++++++ src/message.rs | 14 +++++++++++++ src/receive_imf/tests.rs | 6 +++++- 7 files changed, 102 insertions(+), 22 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 7e7af8dd3..cdcc72274 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4105,6 +4105,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 a001090ae..6ae7d6724 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3368,6 +3368,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 1861807d9..b0b05e222 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::time::Duration; use std::{collections::HashMap, str::FromStr}; @@ -1878,6 +1879,15 @@ impl CommandApi { Ok(can_send) } + /// Saves a file copy at the user-provided path. + /// + /// Fails if file already exists at the provided path. + async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?; + message.save_file(&ctx, Path::new(&path)).await + } + // --------------------------------------------- // functions for the composer // the composer is the message input field @@ -1950,19 +1960,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 b850be2f4..dcc157ada 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -1307,23 +1307,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 a685c35bb..7196aed6f 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 9ca3b4498..3fed2daef 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}; @@ -605,6 +606,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 f58d3f76b..8e16a29cd 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -2994,11 +2994,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;