[fractal/fractal-next] content: Use unique names for media files



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]