Browse Source

utils: Use $XDG_RUNTIME_DIR for temporary files and remove them after use

Using $XDG_RUNTIME_DIR instead of $TMPDIR means that the temporary files
are scoped to the user.
And since those folders are usually backed by a tmpfs in memory, we need
to remove the files as soon as possible to release space.
pipelines/767384
Kévin Commaille 1 year ago
parent
commit
23d0960389
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 1
      Cargo.lock
  2. 1
      Cargo.toml
  3. 12
      src/application.rs
  4. 2
      src/components/avatar/editable.rs
  5. 23
      src/components/media/animated_image_paintable.rs
  6. 17
      src/components/media/content_viewer.rs
  7. 18
      src/session/view/content/room_details/history_viewer/audio_row.rs
  8. 16
      src/session/view/content/room_history/message_row/audio.rs
  9. 8
      src/session/view/content/room_history/message_row/visual_media.rs
  10. 2
      src/session/view/content/room_history/message_toolbar/attachment_dialog.rs
  11. 8
      src/utils/matrix/media_message.rs
  12. 11
      src/utils/media/image/mod.rs
  13. 18
      src/utils/media/image/queue.rs
  14. 2
      src/utils/media/mod.rs
  15. 99
      src/utils/mod.rs

1
Cargo.lock generated

@ -1386,6 +1386,7 @@ dependencies = [
"serde_json",
"sourceview5",
"strum",
"tempfile",
"thiserror",
"tld",
"tokio",

1
Cargo.toml

@ -41,6 +41,7 @@ secular = { version = "1", features = ["bmp", "normalization"] }
serde = "1"
serde_json = "1"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3"
thiserror = "1"
tld = "2"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] }

12
src/application.rs

@ -1,4 +1,4 @@
use std::{cell::RefCell, fmt, rc::Rc};
use std::{borrow::Cow, cell::RefCell, fmt, rc::Rc};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
@ -13,7 +13,7 @@ use crate::{
system_settings::SystemSettings,
toast,
utils::{matrix::MatrixIdUri, BoundObjectWeakRef, LoadingState},
Window,
Window, GETTEXT_PACKAGE,
};
/// The key for the current session setting.
@ -529,6 +529,14 @@ impl AppProfile {
pub fn should_use_devel_class(self) -> bool {
matches!(self, Self::Devel)
}
/// The name of the directory where to put data for this profile.
pub fn dir_name(self) -> Cow<'static, str> {
match self {
AppProfile::Stable => Cow::Borrowed(GETTEXT_PACKAGE),
_ => Cow::Owned(format!("{GETTEXT_PACKAGE}-{self}")),
}
}
}
impl fmt::Display for AppProfile {

2
src/components/avatar/editable.rs

@ -326,7 +326,7 @@ mod imp {
/// Load the temporary paintable from the given file.
pub(super) async fn set_temp_paintable_from_file(&self, file: gio::File) {
let handle = IMAGE_QUEUE
.add_file_request(file, Some(self.avatar_dimensions()))
.add_file_request(file.into(), Some(self.avatar_dimensions()))
.await;
let paintable = handle.await.map(|image| Some(image.into()));
self.set_temp_paintable(paintable);

23
src/components/media/animated_image_paintable.rs

@ -4,7 +4,10 @@ use glycin::{Frame, Image};
use gtk::{gdk, glib, glib::clone, graphene, prelude::*, subclass::prelude::*};
use tracing::error;
use crate::{spawn, spawn_tokio, utils::CountedRef};
use crate::{
spawn, spawn_tokio,
utils::{CountedRef, File},
};
mod imp {
use std::{
@ -18,6 +21,8 @@ mod imp {
pub struct AnimatedImagePaintable {
/// The image loader.
image_loader: OnceCell<Arc<Image<'static>>>,
/// The file of the image.
file: OnceCell<File>,
/// The current frame that is displayed.
pub(super) current_frame: RefCell<Option<Arc<Frame>>>,
/// The next frame of the animation, if any.
@ -97,7 +102,13 @@ mod imp {
}
/// Initialize the image.
pub(super) fn init(&self, image_loader: Arc<Image<'static>>, first_frame: Arc<Frame>) {
pub(super) fn init(
&self,
file: File,
image_loader: Arc<Image<'static>>,
first_frame: Arc<Frame>,
) {
self.file.set(file).expect("file is uninitialized");
self.image_loader
.set(image_loader)
.expect("image loader is uninitialized");
@ -220,10 +231,14 @@ glib::wrapper! {
impl AnimatedImagePaintable {
/// Construct an `AnimatedImagePaintable` with the given loader and first
/// frame.
pub(crate) fn new(image_loader: Arc<Image<'static>>, first_frame: Arc<Frame>) -> Self {
pub(crate) fn new(
file: File,
image_loader: Arc<Image<'static>>,
first_frame: Arc<Frame>,
) -> Self {
let obj = glib::Object::new::<Self>();
obj.imp().init(image_loader, first_frame);
obj.imp().init(file, image_loader, first_frame);
obj
}

17
src/components/media/content_viewer.rs

@ -7,7 +7,7 @@ use super::{AnimatedImagePaintable, AudioPlayer, LocationViewer};
use crate::{
components::ContextMenuBin,
prelude::*,
utils::{media::image::IMAGE_QUEUE, CountedRef},
utils::{media::image::IMAGE_QUEUE, CountedRef, File},
};
/// The types of content supported by the [`MediaContentViewer`].
@ -69,6 +69,8 @@ mod imp {
/// Whether to play the media content automatically.
#[property(get, construct_only)]
autoplay: Cell<bool>,
/// The current media file.
file: RefCell<Option<File>>,
paintable_animation_ref: RefCell<Option<CountedRef>>,
}
@ -110,6 +112,8 @@ mod imp {
/// Show the fallback message for the given content type.
pub(super) fn show_fallback(&self, content_type: ContentType) {
self.file.take();
let title = match content_type {
ContentType::Image => gettext("Image not Viewable"),
ContentType::Audio => gettext("Audio Clip not Playable"),
@ -128,6 +132,7 @@ mod imp {
/// [`MediaContentViewer::view_file()`].
pub(super) fn view_image(&self, image: &gdk::Paintable) {
self.set_visible_child("loading");
self.file.take();
let picture = if let Some(picture) = self.media_child::<gtk::Picture>() {
picture
@ -146,13 +151,15 @@ mod imp {
}
/// View the given file.
pub(super) async fn view_file(&self, file: gio::File, content_type: Option<ContentType>) {
pub(super) async fn view_file(&self, file: File, content_type: Option<ContentType>) {
self.set_visible_child("loading");
self.file.replace(Some(file.clone()));
let content_type = if let Some(content_type) = content_type {
content_type
} else {
let file_info = file
.as_gfile()
.query_info_future(
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FileQueryInfoFlags::NONE,
@ -191,7 +198,7 @@ mod imp {
audio
};
audio.set_file(Some(&file));
audio.set_file(Some(&file.as_gfile()));
self.update_animated_paintable_state();
self.set_visible_child("viewer");
return;
@ -209,7 +216,7 @@ mod imp {
video
};
video.set_file(Some(&file));
video.set_file(Some(&file.as_gfile()));
self.update_animated_paintable_state();
self.set_visible_child("viewer");
return;
@ -312,7 +319,7 @@ impl MediaContentViewer {
/// View the given file.
///
/// If the content type is not provided, it will be guessed from the file.
pub(crate) async fn view_file(&self, file: gio::File, content_type: Option<ContentType>) {
pub(crate) async fn view_file(&self, file: File, content_type: Option<ContentType>) {
self.imp().view_file(file, content_type).await;
}

18
src/session/view/content/room_details/history_viewer/audio_row.rs

@ -1,11 +1,14 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use glib::clone;
use gtk::{gio, glib, CompositeTemplate};
use gtk::{glib, CompositeTemplate};
use tracing::warn;
use super::HistoryViewerEvent;
use crate::{gettext_f, spawn, utils::matrix::MediaMessage};
use crate::{
gettext_f, spawn,
utils::{matrix::MediaMessage, File},
};
mod imp {
use std::cell::RefCell;
@ -23,6 +26,9 @@ mod imp {
/// The audio event.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
pub event: RefCell<Option<HistoryViewerEvent>>,
/// The media file.
file: RefCell<Option<File>>,
/// The API for the media file.
pub media_file: RefCell<Option<gtk::MediaFile>>,
#[template_child]
pub play_button: TemplateChild<gtk::Button>,
@ -95,6 +101,7 @@ mod imp {
}
self.event.replace(event);
self.file.take();
spawn!(clone!(
#[weak(rename_to = imp)]
@ -121,7 +128,7 @@ mod imp {
match media_message.into_tmp_file(&client).await {
Ok(file) => {
self.set_media_file(&file);
self.set_media_file(file);
}
Err(error) => {
warn!("Could not retrieve audio file: {error}");
@ -130,8 +137,8 @@ mod imp {
}
/// Set the media file to play.
fn set_media_file(&self, file: &gio::File) {
let media_file = gtk::MediaFile::for_file(file);
fn set_media_file(&self, file: File) {
let media_file = gtk::MediaFile::for_file(&file.as_gfile());
media_file.connect_error_notify(|media_file| {
if let Some(error) = media_file.error() {
@ -149,6 +156,7 @@ mod imp {
}
));
self.file.replace(Some(file));
self.media_file.replace(Some(media_file));
}
}

16
src/session/view/content/room_history/message_row/audio.rs

@ -1,7 +1,6 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone},
CompositeTemplate,
};
@ -13,7 +12,7 @@ use crate::{
gettext_f,
session::model::Session,
spawn,
utils::{matrix::MediaMessage, LoadingState},
utils::{matrix::MediaMessage, File, LoadingState},
};
mod imp {
@ -32,6 +31,8 @@ mod imp {
/// The filename of the audio file.
#[property(get)]
pub filename: RefCell<Option<String>>,
/// The media file.
pub(super) file: RefCell<Option<File>>,
/// The state of the audio file.
#[property(get, builder(LoadingState::default()))]
pub state: Cell<LoadingState>,
@ -151,6 +152,7 @@ impl MessageAudio {
/// Display the given `audio` message.
pub fn audio(&self, message: MediaMessage, session: &Session, format: ContentFormat) {
self.imp().file.take();
self.set_filename(Some(message.filename()));
let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
@ -172,7 +174,7 @@ impl MessageAudio {
async move {
match message.into_tmp_file(&client).await {
Ok(file) => {
obj.display_file(&file);
obj.display_file(file);
}
Err(error) => {
warn!("Could not retrieve audio file: {error}");
@ -184,8 +186,8 @@ impl MessageAudio {
);
}
fn display_file(&self, file: &gio::File) {
let media_file = gtk::MediaFile::for_file(file);
fn display_file(&self, file: File) {
let media_file = gtk::MediaFile::for_file(&file.as_gfile());
media_file.connect_error_notify(clone!(
#[weak(rename_to = obj)]
@ -198,7 +200,9 @@ impl MessageAudio {
}
));
self.imp().player.set_media_file(Some(media_file));
let imp = self.imp();
imp.file.replace(Some(file));
imp.player.set_media_file(Some(media_file));
self.set_state(LoadingState::Ready);
}
}

8
src/session/view/content/room_history/message_row/visual_media.rs

@ -21,7 +21,7 @@ use crate::{
image::{ImageRequestPriority, ThumbnailSettings},
FrameDimensions,
},
CountedRef, LoadingState,
CountedRef, File, LoadingState,
},
};
@ -72,6 +72,8 @@ mod imp {
/// Whether to display this media in a compact format.
#[property(get)]
compact: Cell<bool>,
/// The current video file, if any.
file: RefCell<Option<File>>,
paintable_animation_ref: RefCell<Option<CountedRef>>,
}
@ -272,6 +274,7 @@ mod imp {
session: &Session,
format: ContentFormat,
) {
self.file.take();
self.dimensions.set(media_message.dimensions());
let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
@ -395,7 +398,8 @@ mod imp {
};
child.set_compact(self.compact.get());
child.play_video_file(file);
child.play_video_file(file.as_gfile());
self.file.replace(Some(file));
}
/// Set the given error message for this media.

2
src/session/view/content/room_history/message_toolbar/attachment_dialog.rs

@ -114,7 +114,7 @@ impl AttachmentDialog {
#[weak]
imp,
async move {
imp.media.view_file(file, None).await;
imp.media.view_file(file.into(), None).await;
imp.set_loading(false);
}
));

8
src/utils/matrix/media_message.rs

@ -21,7 +21,7 @@ use crate::{
},
FrameDimensions, MediaFileError,
},
save_data_to_tmp_file,
save_data_to_tmp_file, File,
},
};
@ -128,9 +128,9 @@ impl MediaMessage {
/// temporary file.
///
/// Returns an error if something occurred while fetching the content.
pub async fn into_tmp_file(self, client: &Client) -> Result<gio::File, MediaFileError> {
pub async fn into_tmp_file(self, client: &Client) -> Result<File, MediaFileError> {
let data = self.into_content(client).await?;
Ok(save_data_to_tmp_file(&data)?)
Ok(save_data_to_tmp_file(data).await?)
}
/// Save the content of the media to a file selected by the user.
@ -337,7 +337,7 @@ impl VisualMediaMessage {
///
/// Returns an error if something occurred while fetching the content or
/// saving the content to a file.
pub async fn into_tmp_file(self, client: &Client) -> Result<gio::File, MediaFileError> {
pub async fn into_tmp_file(self, client: &Client) -> Result<File, MediaFileError> {
MediaMessage::from(self).into_tmp_file(client).await
}

11
src/utils/media/image/mod.rs

@ -27,7 +27,7 @@ mod queue;
pub(crate) use queue::{ImageRequestPriority, IMAGE_QUEUE};
use super::{FrameDimensions, MediaFileError};
use crate::{components::AnimatedImagePaintable, spawn_tokio, DISABLE_GLYCIN_SANDBOX};
use crate::{components::AnimatedImagePaintable, spawn_tokio, utils::File, DISABLE_GLYCIN_SANDBOX};
/// The maximum dimensions of a generated thumbnail.
const THUMBNAIL_MAX_DIMENSIONS: FrameDimensions = FrameDimensions {
@ -74,10 +74,10 @@ async fn image_loader(file: gio::File) -> Result<glycin::Image<'static>, glycin:
/// Set `request_dimensions` if the image will be shown at specific dimensions.
/// To show the image at its natural size, set it to `None`.
async fn load_image(
file: gio::File,
file: File,
request_dimensions: Option<FrameDimensions>,
) -> Result<Image, glycin::ErrorCtx> {
let image_loader = image_loader(file).await?;
let image_loader = image_loader(file.as_gfile()).await?;
let frame_request = request_dimensions.map(|request| {
let image_info = image_loader.info();
@ -97,6 +97,7 @@ async fn load_image(
image_loader.next_frame().await?
};
Ok(Image {
file,
loader: image_loader.into(),
first_frame: first_frame.into(),
})
@ -108,6 +109,8 @@ async fn load_image(
/// An image that was just loaded.
#[derive(Clone)]
pub struct Image {
/// The file of the image.
file: File,
/// The image loader.
loader: Arc<glycin::Image<'static>>,
/// The first frame of the image.
@ -123,7 +126,7 @@ impl fmt::Debug for Image {
impl From<Image> for gdk::Paintable {
fn from(value: Image) -> Self {
if value.first_frame.delay().is_some() {
AnimatedImagePaintable::new(value.loader, value.first_frame).upcast()
AnimatedImagePaintable::new(value.file, value.loader, value.first_frame).upcast()
} else {
value.first_frame.texture().upcast()
}

18
src/utils/media/image/queue.rs

@ -8,14 +8,14 @@ use std::{
};
use futures_util::future::BoxFuture;
use gtk::{gio, glib, prelude::*};
use gtk::glib;
use matrix_sdk::{
media::{MediaRequest, UniqueKey},
Client,
};
use tokio::{
sync::{broadcast, Mutex as AsyncMutex},
task::{spawn_blocking, AbortHandle},
task::AbortHandle,
};
use tracing::{debug, trace, warn};
@ -24,7 +24,7 @@ use crate::{
spawn_tokio,
utils::{
media::{FrameDimensions, MediaFileError},
save_data_to_tmp_file,
save_data_to_tmp_file, File,
},
};
@ -118,7 +118,7 @@ impl ImageRequestQueue {
/// same request.
pub async fn add_file_request(
&self,
file: gio::File,
file: File,
dimensions: Option<FrameDimensions>,
) -> ImageRequestHandle {
let inner = self.inner.clone();
@ -227,7 +227,7 @@ impl ImageRequestQueueInner {
/// same request.
fn add_file_request(
&mut self,
file: gio::File,
file: File,
dimensions: Option<FrameDimensions>,
) -> ImageRequestHandle {
let data = FileRequestData { file, dimensions };
@ -515,7 +515,7 @@ impl DownloadRequestData {
}
impl IntoFuture for DownloadRequestData {
type Output = Result<gio::File, MediaFileError>;
type Output = Result<File, MediaFileError>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
@ -532,9 +532,7 @@ impl IntoFuture for DownloadRequestData {
}
};
let file = spawn_blocking(move || save_data_to_tmp_file(&data))
.await
.expect("task was not aborted")?;
let file = save_data_to_tmp_file(data).await?;
Ok(file)
})
}
@ -544,7 +542,7 @@ impl IntoFuture for DownloadRequestData {
#[derive(Clone)]
struct FileRequestData {
/// The image file to load.
file: gio::File,
file: File,
/// The dimensions to request.
dimensions: Option<FrameDimensions>,
}

2
src/utils/media/mod.rs

@ -142,7 +142,7 @@ pub enum MediaFileError {
/// An error occurred when downloading the media.
Sdk(#[from] matrix_sdk::Error),
/// An error occurred when writing the media to a file.
File(#[from] glib::Error),
File(#[from] std::io::Error),
}
/// The dimensions of a frame.

99
src/utils/mod.rs

@ -14,12 +14,12 @@ pub mod string;
pub mod template_callbacks;
use std::{
borrow::Cow,
cell::{Cell, OnceCell, RefCell},
fmt,
path::PathBuf,
fmt, fs,
io::{self, Write},
path::{Path, PathBuf},
rc::{Rc, Weak},
sync::LazyLock,
sync::{Arc, LazyLock},
};
use futures_util::{
@ -28,6 +28,7 @@ use futures_util::{
};
use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*};
use regex::Regex;
use tempfile::NamedTempFile;
pub use self::{
dummy_object::DummyObject,
@ -35,21 +36,16 @@ pub use self::{
location::{Location, LocationError, LocationExt},
single_item_list_model::SingleItemListModel,
};
use crate::{AppProfile, GETTEXT_PACKAGE, PROFILE, RUNTIME};
use crate::{PROFILE, RUNTIME};
/// The path of the directory where data should be stored, depending on its
/// type.
pub fn data_dir_path(data_type: DataType) -> PathBuf {
let dir_name = match PROFILE {
AppProfile::Stable => Cow::Borrowed(GETTEXT_PACKAGE),
_ => Cow::Owned(format!("{GETTEXT_PACKAGE}-{PROFILE}")),
};
let mut path = match data_type {
DataType::Persistent => glib::user_data_dir(),
DataType::Cache => glib::user_cache_dir(),
};
path.push(dir_name.as_ref());
path.push(PROFILE.dir_name().as_ref());
path
}
@ -520,18 +516,77 @@ pub fn add_activate_binding_action<T: WidgetClassExt>(klass: &mut T, action: &st
}
}
/// A wrapper around several sources of files.
#[derive(Debug, Clone)]
pub enum File {
/// A `GFile`.
Gio(gio::File),
/// A temporary file.
///
/// When all strong references to this file are destroyed, the file will be
/// destroyed too.
Temp(Arc<NamedTempFile>),
}
impl File {
/// The path to the file.
pub(crate) fn path(&self) -> Option<PathBuf> {
match self {
Self::Gio(file) => file.path(),
Self::Temp(file) => Some(file.path().to_owned()),
}
}
/// Get a `GFile` for this file.
pub(crate) fn as_gfile(&self) -> gio::File {
match self {
Self::Gio(file) => file.clone(),
Self::Temp(file) => gio::File::for_path(file.path()),
}
}
}
impl From<gio::File> for File {
fn from(value: gio::File) -> Self {
Self::Gio(value)
}
}
impl From<NamedTempFile> for File {
fn from(value: NamedTempFile) -> Self {
Self::Temp(value.into())
}
}
/// The directory where to put temporary files.
static TMP_DIR: LazyLock<Box<Path>> = LazyLock::new(|| {
let mut dir = glib::user_runtime_dir();
dir.push(PROFILE.dir_name().as_ref());
dir.into_boxed_path()
});
/// Save the given data to a temporary file.
pub fn save_data_to_tmp_file(data: &[u8]) -> Result<gio::File, glib::Error> {
let (file, _) = gio::File::new_tmp(None::<String>)?;
file.replace_contents(
data,
None,
false,
gio::FileCreateFlags::REPLACE_DESTINATION,
gio::Cancellable::NONE,
)?;
Ok(file)
///
/// When all strong references to the returned file are destroyed, the file will
/// be destroyed too.
pub(crate) async fn save_data_to_tmp_file(data: Vec<u8>) -> Result<File, std::io::Error> {
RUNTIME
.spawn_blocking(move || {
let dir = TMP_DIR.as_ref();
if !dir.exists() {
if let Err(error) = fs::create_dir(dir) {
if !matches!(error.kind(), io::ErrorKind::AlreadyExists) {
return Err(error);
}
}
}
let mut file = NamedTempFile::new_in(dir)?;
file.write_all(&data)?;
Ok(file.into())
})
.await
.expect("task was not aborted")
}
/// A counted reference.

Loading…
Cancel
Save