[fractal/fractal-next] content: Use unique names for media files
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] content: Use unique names for media files
- Date: Thu, 13 Jan 2022 11:26:09 +0000 (UTC)
commit 9ef753950d2401cba2f4318a76375a2d7a89c8ac
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Sat Dec 25 13:59:02 2021 +0100
content: Use unique names for media files
Avoids conflicts when several files have the same name.
Avoids errors when the filename is not set.
Cargo.lock | 11 ++++
Cargo.toml | 1 +
.../content/room_history/message_row/media.rs | 14 ++---
.../content/room_history/message_row/mod.rs | 15 +++++-
src/session/media_viewer.rs | 6 +--
src/session/room/event.rs | 59 ++++++++++++++++++----
src/session/room/event_actions.rs | 11 +++-
src/utils.rs | 56 ++++++++++++++++++++
8 files changed, 151 insertions(+), 22 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 80c329d6..535c7716 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 83f44389..cd10e056 100644
--- a/Cargo.toml
+++ b/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"
diff --git a/src/session/content/room_history/message_row/media.rs
b/src/session/content/room_history/message_row/media.rs
index a7cbad5a..b8cf64df 100644
--- a/src/session/content/room_history/message_row/media.rs
+++ b/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);
diff --git a/src/session/content/room_history/message_row/mod.rs
b/src/session/content/room_history/message_row/mod.rs
index c6359641..f5a234a4 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/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>())
diff --git a/src/session/media_viewer.rs b/src/session/media_viewer.rs
index 9dd65536..f63a3561 100644
--- a/src/session/media_viewer.rs
+++ b/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,
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index 8052a64c..cb717506 100644
--- a/src/session/room/event.rs
+++ b/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));
}
_ => {}
};
diff --git a/src/session/room/event_actions.rs b/src/session/room/event_actions.rs
index d5bc145d..37d9d225 100644
--- a/src/session/room/event_actions.rs
+++ b/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);
diff --git a/src/utils.rs b/src/utils.rs
index dad489e6..c392bdf7 100644
--- a/src/utils.rs
+++ b/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)
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]