Skip to content

Commit

Permalink
api: Add dc_msg_save_file() which saves file copy at the provided path (
Browse files Browse the repository at this point in the history
#4309)

- Deprecate dc_msg_get_file(). It's kept for compatibility and now creates a temporary directory
  inside the blobdir, saves a file copy with unmangled filename, and returns its path. In the
  housekeeping we regularly clear these directories.
- Add dc_msg_save_file() which saves file copy at the provided path 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().
- Make dc_msg_get_filename() return the original filename for the use as a default in the file
  saving dialog. For this purpose add Param::Filename in addition to Param::File, to store the
  original filename. Param::Filename is used as an attachment filename in sent emails.
- Use random filename suffixes for blobstorage. Thanks to the added Param::Filename these random
  suffixes is not sent over the network.
  • Loading branch information
iequidoo committed Jul 25, 2023
1 parent f930576 commit c211f3a
Show file tree
Hide file tree
Showing 19 changed files with 285 additions and 129 deletions.
18 changes: 17 additions & 1 deletion deltachat-ffi/deltachat.h
Original file line number Diff line number Diff line change
Expand Up @@ -3986,22 +3986,38 @@ char* dc_msg_get_text (const dc_msg_t* msg);
*/
char* dc_msg_get_subject (const dc_msg_t* msg);


/**
* Find out full path, file name and extension of the file associated with a
* message.
*
* Typically files are associated with images, videos, audios, documents.
* Plain text messages do not have a file.
*
* @deprecated Deprecated 2023-07-22, use dc_msg_save_file() instead.
* @memberof dc_msg_t
* @param msg The message object.
* @return The full path, the file name, and the extension of the file associated with the message.
* If there is no file associated with the message, an empty string is returned.
* NULL is never returned and the returned value must be released using dc_str_unref().
* On error NULL is returned.
* The returned value must be released using dc_str_unref().
*/
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 a base file name without the path. The base file name includes the extension; the path
* is not returned. To get the full path, use dc_msg_get_file().
Expand Down
41 changes: 36 additions & 5 deletions deltachat-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3310,11 +3310,42 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
}
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
ffi_msg
.message
.get_file(ctx)
.map(|p| p.to_string_lossy().strdup())
.unwrap_or_else(|| "".strdup())
let file_path = block_on(async move { ffi_msg.message.get_file(ctx).await });
match file_path {
Ok(Some(p)) => p.to_string_lossy().strdup(),
Ok(None) => "".strdup(),
Err(err) => {
eprintln!("Failed to get file from message: {err:#}");
ptr::null_mut()
}
}
}

#[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(async move {
ffi_msg
.message
.save_file(ctx, &std::path::PathBuf::from(path))
.await
});
match r {
Ok(()) => 1,
Err(err) => {
eprintln!("Failed to save file from message: {err:#}");
0
}
}
}

#[no_mangle]
Expand Down
29 changes: 16 additions & 13 deletions deltachat-jsonrpc/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};

Expand Down Expand Up @@ -1891,19 +1892,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(())
}

Expand Down
6 changes: 3 additions & 3 deletions deltachat-jsonrpc/src/api/types/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl MessageObject {
|| quote.get_viewtype() == Viewtype::Gif
|| quote.get_viewtype() == Viewtype::Sticker
{
match quote.get_file(context) {
match quote.get_file_path(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}
Expand Down Expand Up @@ -221,7 +221,7 @@ impl MessageObject {

setup_code_begin: message.get_setupcodebegin(context).await,

file: match message.get_file(context) {
file: match message.get_file_path(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}, //BLOBS
Expand Down Expand Up @@ -416,7 +416,7 @@ impl MessageNotificationInfo {
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
) {
message
.get_file(context)
.get_file_path(context)
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
.unwrap_or_default()
} else {
Expand Down
3 changes: 2 additions & 1 deletion node/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () {
context.setChatProfileImage(chatId, imagePath)
const blobPath = context.getChat(chatId).getProfileImage()
expect(blobPath.startsWith(blobs)).to.be.true
expect(blobPath.endsWith(image)).to.be.true
expect(blobPath.includes('image')).to.be.true
expect(blobPath.endsWith('.jpeg')).to.be.true

context.setChatProfileImage(chatId, null)
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
Expand Down
19 changes: 12 additions & 7 deletions python/tests/test_1_online.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)

basename = "somedäüta.html.zip"
p = tmp_path / basename
basename = "somedäüta"
ext = ".html.zip"
p = tmp_path / (basename + ext)
p.write_text("some data")

def send_and_receive_message():
Expand All @@ -181,23 +182,26 @@ def send_and_receive_message():
msg = send_and_receive_message()
assert msg.text == "withfile"
assert open(msg.filename).read() == "some data"
assert msg.filename.endswith(basename)
msg.filename.index(basename)
assert msg.filename.endswith(ext)

msg2 = send_and_receive_message()
assert msg2.text == "withfile"
assert open(msg2.filename).read() == "some data"
assert msg2.filename.endswith("html.zip")
msg2.filename.index(basename)
assert msg2.filename.endswith(ext)
assert msg.filename != msg2.filename


def test_send_file_html_attachment(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)

basename = "test.html"
basename = "test"
ext = ".html"
content = "<html><body>text</body>data"

p = tmp_path / basename
p = tmp_path / (basename + ext)
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
p.write_text(content)
Expand All @@ -211,7 +215,8 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
msg = ac2.get_message_by_id(ev.data2)

assert open(msg.filename).read() == content
assert msg.filename.endswith(basename)
msg.filename.index(basename)
assert msg.filename.endswith(ext)


def test_html_message(acfactory, lp):
Expand Down
7 changes: 3 additions & 4 deletions python/tests/test_2_increation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.write_text("hello there\n")
chat.send_file(str(src))

blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
msg = chat.send_file(str(src))
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
assert msg.filename.endswith(".txt")

def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
Expand Down
Loading

0 comments on commit c211f3a

Please sign in to comment.