Browse Source

content: Use unique names for media files

Avoids conflicts when several files have the same name.

Avoids errors when the filename is not set.
merge-requests/1327/merge
Kévin Commaille 4 years ago committed by Julian Sparber
parent
commit
9ef753950d
  1. 11
      Cargo.lock
  2. 1
      Cargo.toml
  3. 14
      src/session/content/room_history/message_row/media.rs
  4. 15
      src/session/content/room_history/message_row/mod.rs
  5. 6
      src/session/media_viewer.rs
  6. 59
      src/session/room/event.rs
  7. 11
      src/session/room/event_actions.rs
  8. 56
      src/utils.rs

11
Cargo.lock generated

@ -998,6 +998,7 @@ dependencies = [
"log",
"matrix-sdk",
"mime",
"mime_guess",
"once_cell",
"qrcode",
"rand 0.8.4",
@ -2340,6 +2341,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.3.7"

1
Cargo.toml

@ -35,6 +35,7 @@ gst_base = {version = "0.17", package = "gstreamer-base"}
gst_video = {version = "0.17", package = "gstreamer-video"}
image = {version = "0.23", default-features = false, features=["png"]}
regex = "1.5.4"
mime_guess = "2.0.3"
[dependencies.sourceview]
package = "sourceview5"

14
src/session/content/room_history/message_row/media.rs

@ -25,7 +25,7 @@ use crate::{
components::VideoPlayer,
session::Session,
spawn, spawn_tokio,
utils::{cache_dir, uint_to_i32},
utils::{cache_dir, media_type_uid, uint_to_i32},
};
const MAX_THUMBNAIL_WIDTH: i32 = 600;
@ -378,9 +378,11 @@ impl MessageMedia {
};
if let Some(data) = thumbnail {
Ok(Some(data))
let id = media_type_uid(content.thumbnail());
Ok((Some(data), id))
} else {
client.get_file(content, true).await
let id = media_type_uid(content.file());
client.get_file(content, true).await.map(|data| (data, id))
}
});
@ -390,7 +392,7 @@ impl MessageMedia {
let priv_ = imp::MessageMedia::from_instance(&obj);
match handle.await.unwrap() {
Ok(Some(data)) => {
Ok((Some(data), id)) => {
match media_type {
MediaType::Image | MediaType::Sticker => {
let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&data));
@ -421,7 +423,7 @@ impl MessageMedia {
// we need to store the file.
// See: https://gitlab.gnome.org/GNOME/gtk/-/issues/4062
let mut path = cache_dir();
path.push(body.unwrap());
path.push(format!("{}_{}", id, body.unwrap_or_default()));
let file = gio::File::for_path(path);
file.replace_contents(
&data,
@ -450,7 +452,7 @@ impl MessageMedia {
obj.set_state(MediaState::Ready);
}
Ok(None) => {
Ok((None, _)) => {
warn!("Could not retrieve invalid media file");
priv_.overlay_error.set_tooltip_text(Some(&gettext("Could not retrieve media")));
obj.set_state(MediaState::Error);

15
src/session/content/room_history/message_row/mod.rs

@ -2,7 +2,7 @@ mod file;
mod media;
mod text;
use crate::components::Avatar;
use crate::{components::Avatar, utils::filename_for_mime};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
@ -217,7 +217,18 @@ impl MessageRow {
child.emote(message.formatted, message.body, event.sender());
}
MessageType::File(message) => {
let filename = message.filename.unwrap_or(message.body);
let info = message.info.as_ref();
let filename = message
.filename
.filter(|name| !name.is_empty())
.or(Some(message.body))
.filter(|name| !name.is_empty())
.unwrap_or_else(|| {
filename_for_mime(
info.and_then(|info| info.mimetype.as_deref()),
None,
)
});
let child = if let Some(Ok(child)) =
priv_.content.child().map(|w| w.downcast::<MessageFile>())

6
src/session/media_viewer.rs

@ -270,7 +270,7 @@ impl MediaViewer {
let priv_ = imp::MediaViewer::from_instance(&obj);
match event.get_media_content().await {
Ok((_, data)) => {
Ok((_, _, data)) => {
let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&data));
let texture = Pixbuf::from_stream(&stream, gio::NONE_CANCELLABLE)
.ok()
@ -297,12 +297,12 @@ impl MediaViewer {
let priv_ = imp::MediaViewer::from_instance(&obj);
match event.get_media_content().await {
Ok((_, data)) => {
Ok((uid, filename, data)) => {
// The GStreamer backend of GtkVideo doesn't work with input streams so
// we need to store the file.
// See: https://gitlab.gnome.org/GNOME/gtk/-/issues/4062
let mut path = cache_dir();
path.push(video.body);
path.push(format!("{}_{}", uid, filename));
let file = gio::File::for_path(path);
file.replace_contents(
&data,

59
src/session/room/event.rs

@ -2,6 +2,7 @@ use gtk::{glib, glib::clone, glib::DateTime, prelude::*, subclass::prelude::*};
use log::warn;
use matrix_sdk::{
deserialized_responses::SyncRoomEvent,
media::MediaEventContent,
ruma::{
events::{
room::message::Relation,
@ -17,6 +18,7 @@ use matrix_sdk::{
use crate::{
session::{room::Member, Room},
spawn_tokio,
utils::{filename_for_mime, media_type_uid},
};
#[derive(Clone, Debug, glib::GBoxed)]
@ -622,29 +624,68 @@ impl Event {
/// - Image message (`MessageType::Image`).
/// - Video message (`MessageType::Video`).
///
/// Returns `Ok((filename, binary_content))` on success, `Err` if an error occured while
/// fetching the content. Panics on an incompatible event.
pub async fn get_media_content(&self) -> Result<(String, Vec<u8>), matrix_sdk::Error> {
/// Returns `Ok((uid, filename, binary_content))` on success, `Err` if an error occured while
/// fetching the content. Panics on an incompatible event. `uid` is a unique identifier for this
/// media.
pub async fn get_media_content(&self) -> Result<(String, String, Vec<u8>), matrix_sdk::Error> {
if let AnyMessageEventContent::RoomMessage(content) = self.message_content().unwrap() {
let client = self.room().session().client();
match content.msgtype {
MessageType::File(content) => {
let filename = content.filename.clone().unwrap_or(content.body.clone());
let uid = media_type_uid(content.file());
let filename = content
.filename
.as_ref()
.filter(|name| !name.is_empty())
.or(Some(&content.body))
.filter(|name| !name.is_empty())
.map(|name| name.clone())
.unwrap_or_else(|| {
filename_for_mime(
content
.info
.as_ref()
.and_then(|info| info.mimetype.as_deref()),
None,
)
});
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((filename, data));
return Ok((uid, filename, data));
}
MessageType::Image(content) => {
let filename = content.body.clone();
let uid = media_type_uid(content.file());
let filename = if content.body.is_empty() {
filename_for_mime(
content
.info
.as_ref()
.and_then(|info| info.mimetype.as_deref()),
Some(mime::IMAGE),
)
} else {
content.body.clone()
};
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((filename, data));
return Ok((uid, filename, data));
}
MessageType::Video(content) => {
let filename = content.body.clone();
let uid = media_type_uid(content.file());
let filename = if content.body.is_empty() {
filename_for_mime(
content
.info
.as_ref()
.and_then(|info| info.mimetype.as_deref()),
Some(mime::VIDEO),
)
} else {
content.body.clone()
};
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((filename, data));
return Ok((uid, filename, data));
}
_ => {}
};

11
src/session/room/event_actions.rs

@ -91,7 +91,7 @@ where
spawn!(
glib::PRIORITY_LOW,
clone!(@weak window => async move {
let (filename, data) = match event.get_media_content().await {
let (_, filename, data) = match event.get_media_content().await {
Ok(res) => res,
Err(err) => {
error!("Could not get file: {}", err);
@ -148,7 +148,7 @@ where
spawn!(
glib::PRIORITY_LOW,
clone!(@weak window => async move {
let (filename, data) = match event.get_media_content().await {
let (uid, filename, data) = match event.get_media_content().await {
Ok(res) => res,
Err(err) => {
error!("Could not get file: {}", err);
@ -168,6 +168,13 @@ where
};
let mut path = cache_dir();
path.push(uid);
if !path.exists() {
let dir = gio::File::for_path(path.clone());
dir.make_directory_with_parents(gio::NONE_CANCELLABLE)
.unwrap();
}
path.push(filename);
let file = gio::File::for_path(path);

56
src/utils.rs

@ -62,10 +62,14 @@ macro_rules! spawn_tokio {
use std::convert::TryInto;
use std::path::PathBuf;
use std::str::FromStr;
use gettextrs::gettext;
use gtk::gio::{self, prelude::*};
use gtk::glib::{self, Object};
use matrix_sdk::media::MediaType;
use matrix_sdk::ruma::UInt;
use mime::Mime;
/// Returns an expression looking up the given property on `object`.
pub fn prop_expr<T: IsA<Object>>(object: &T, prop: &str) -> gtk::Expression {
@ -161,3 +165,55 @@ pub fn style_scheme() -> Option<sourceview::StyleScheme> {
sourceview::StyleSchemeManager::default().and_then(|scm| scm.scheme(scheme_name))
}
/// Get the unique id of the given `MediaType`.
///
/// It is built from the underlying `MxcUri` and can be safely used in a filename.
///
/// The id is not guaranteed to be unique for malformed `MxcUri`s.
pub fn media_type_uid(media_type: Option<MediaType>) -> String {
if let Some(mxc) = media_type
.map(|media_type| match media_type {
MediaType::Uri(uri) => uri,
MediaType::Encrypted(file) => file.url,
})
.filter(|mxc| mxc.is_valid())
{
format!("{}_{}", mxc.server_name().unwrap(), mxc.media_id().unwrap())
} else {
"media_uid".to_owned()
}
}
/// Get a default filename for a mime type.
///
/// Tries to guess the file extension, but it might not find it.
///
/// If the mime type is unknown, it uses the name for `fallback`. The fallback
/// mime types that are recognized are `mime::IMAGE`, `mime::VIDEO`
/// and `mime::AUDIO`, other values will behave the same as `None`.
pub fn filename_for_mime(mime_type: Option<&str>, fallback: Option<mime::Name>) -> String {
let (type_, extension) = if let Some(mime) = mime_type.and_then(|m| Mime::from_str(m).ok()) {
let extension =
mime_guess::get_mime_extensions(&mime).map(|extensions| extensions[0].to_owned());
(Some(mime.type_().as_str().to_owned()), extension)
} else {
(fallback.map(|type_| type_.as_str().to_owned()), None)
};
let name = match type_.as_deref() {
// Translators: Default name for image files.
Some("image") => gettext("image"),
// Translators: Default name for video files.
Some("video") => gettext("video"),
// Translators: Default name for audio files.
Some("audio") => gettext("audio"),
// Translators: Default name for files.
_ => gettext("file"),
};
extension
.map(|extension| format!("{}.{}", name, extension))
.unwrap_or(name)
}

Loading…
Cancel
Save