diff --git a/data/com.ranfdev.DistroShelf.metainfo.xml.in b/data/com.ranfdev.DistroShelf.metainfo.xml.in index a926c95..bfadd44 100644 --- a/data/com.ranfdev.DistroShelf.metainfo.xml.in +++ b/data/com.ranfdev.DistroShelf.metainfo.xml.in @@ -71,6 +71,14 @@ + + +
    +
  • Fixed custom home path resolution
  • +
  • Add parsing of volume paths
  • +
+
+
    diff --git a/meson.build b/meson.build index 489aa62..2985dde 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('distroshelf', 'rust', - version: '1.0.7', + version: '1.0.8', meson_version: '>= 1.0.0', default_options: [ 'warning_level=2', 'werror=false', ], ) diff --git a/src/config.rs b/src/config.rs index 94861a5..5395043 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -pub static VERSION: &str = "1.0.7"; +pub static VERSION: &str = "1.0.8"; pub static GETTEXT_PACKAGE: &str = "distroshelf"; pub static LOCALEDIR: &str = "/app/share/locale"; pub static PKGDATADIR: &str = "/app/share/distroshelf"; diff --git a/src/create_distrobox_dialog.rs b/src/create_distrobox_dialog.rs index 43f5740..8ccc777 100644 --- a/src/create_distrobox_dialog.rs +++ b/src/create_distrobox_dialog.rs @@ -2,6 +2,7 @@ use adw::prelude::*; use adw::subclass::prelude::*; use gtk::gio::File; use gtk::{gio, glib}; +use tracing::error; use crate::distrobox::{self, CreateArgName, CreateArgs, Error}; use crate::root_store::RootStore; @@ -15,7 +16,7 @@ use gtk::glib::{derived_properties, Properties}; pub enum FileRowSelection { File, - Folder + Folder, } mod imp { use super::*; @@ -41,7 +42,6 @@ mod imp { pub init_row: adw::SwitchRow, pub volume_rows: Rc>>, pub scrolled_window: gtk::ScrolledWindow, - pub current_create_args: RefCell, } #[derived_properties] @@ -50,7 +50,6 @@ mod imp { self.obj().set_title("Create a Distrobox"); self.obj().set_content_width(480); - let toolbar_view = adw::ToolbarView::new(); let header = adw::HeaderBar::new(); @@ -99,24 +98,29 @@ mod imp { self.image_row.set_use_subtitle(true); let obj = self.obj().clone(); - let home_row = self.obj().build_file_row("Select Home Directory", FileRowSelection::Folder, move |path| { - obj.set_home_folder(Some(path.display().to_string())); - }); + let home_row = self.obj().build_file_row( + "Select Home Directory", + FileRowSelection::Folder, + move |path| { + obj.set_home_folder(Some(path.display().to_string())); + }, + ); self.home_row_expander.set_title("Custom Home Directory"); self.home_row_expander.set_show_enable_switch(true); self.home_row_expander.set_enable_expansion(false); self.home_row_expander.add_row(&home_row); let obj = self.obj().clone(); - self.home_row_expander.connect_enable_expansion_notify(clone!( - #[weak] - home_row, - move |expander| { - if !expander.enables_expansion() { - obj.set_home_folder(None::<&str>); + self.home_row_expander + .connect_enable_expansion_notify(clone!( + #[weak] + home_row, + move |expander| { + if !expander.enables_expansion() { + obj.set_home_folder(None::<&str>); + } + home_row.set_subtitle(obj.home_folder().as_deref().unwrap_or("")); } - home_row.set_subtitle(obj.home_folder().as_deref().unwrap_or("")); - } - )); + )); let nvidia_row = adw::SwitchRow::new(); nvidia_row.set_title("NVIDIA Support"); @@ -142,13 +146,13 @@ mod imp { #[weak] obj, move |_| { - let res = obj.update_create_args(); - obj.update_errors(&res); - if let Ok(()) = res { - obj.root_store().create_container( - obj.imp().current_create_args.borrow().clone(), - ); - } + glib::MainContext::ref_thread_default().spawn_local(async move { + let res = obj.extract_create_args().await; + obj.update_errors(&res); + if let Ok(create_args) = res { + obj.root_store().create_container(create_args); + } + }); } )); create_btn.add_css_class("suggested-action"); @@ -169,9 +173,13 @@ mod imp { assemble_group.set_description(Some("Create a container from an assemble file")); let obj = self.obj().clone(); - let file_row = self.obj().build_file_row("Select Assemble File", FileRowSelection::File, move |path| { - obj.set_assemble_file(Some(path.display().to_string())); - }); + let file_row = self.obj().build_file_row( + "Select Assemble File", + FileRowSelection::File, + move |path| { + obj.set_assemble_file(Some(path.display().to_string())); + }, + ); assemble_group.add(&file_row); assemble_page.append(&assemble_group); @@ -202,8 +210,6 @@ mod imp { create_btn.set_sensitive(obj.assemble_file().is_some()); }); - - // Create page for URL creation let url_page = gtk::Box::new(gtk::Orientation::Vertical, 12); url_page.set_margin_start(12); @@ -246,7 +252,8 @@ mod imp { #[weak] obj, move |_| { - obj.root_store().assemble_container(&obj.assemble_url().as_ref().unwrap()); + obj.root_store() + .assemble_container(&obj.assemble_url().as_ref().unwrap()); obj.close(); } )); @@ -255,7 +262,6 @@ mod imp { create_btn.set_sensitive(obj.assemble_url().is_some()); }); - // Add pages to view stack view_stack.add_titled(&gui_page, Some("create"), "Guided"); view_stack.add_titled(&assemble_page, Some("assemble-file"), "From File"); @@ -327,58 +333,53 @@ impl CreateDistroboxDialog { this } - pub fn build_file_row(&self, title: &str, selection: FileRowSelection, cb: impl Fn(PathBuf) + Clone + 'static) -> adw::ActionRow { - let row = adw::ActionRow::new(); - row.set_title(title); - row.set_subtitle("No file selected"); - row.set_activatable(true); - - let file_icon = gtk::Image::from_icon_name("document-open-symbolic"); - row.add_suffix(&file_icon); - - let title = title.to_owned(); - let dialog_cb = clone!( - #[weak] - row, - move |res: Result| { - if let Ok(file) = res { - if let Some(path) = file.path() { - row.set_subtitle(&path.display().to_string()); - cb(path); - } - } - } - ); - row.connect_activated( - move |_| { - let file_dialog = gtk::FileDialog::builder() - .title(&title) - .modal(true) - .build(); - let dialog_cb = dialog_cb.clone(); - match selection { - FileRowSelection::File => { - file_dialog.open( - None::<>k::Window>, - None::<&gio::Cancellable>, - dialog_cb, - ); - } - FileRowSelection::Folder => { - file_dialog.select_folder( - None::<>k::Window>, - None::<&gio::Cancellable>, - dialog_cb, - ); - } + pub fn build_file_row( + &self, + title: &str, + selection: FileRowSelection, + cb: impl Fn(PathBuf) + Clone + 'static, + ) -> adw::ActionRow { + let row = adw::ActionRow::new(); + row.set_title(title); + row.set_subtitle("No file selected"); + row.set_activatable(true); + + let file_icon = gtk::Image::from_icon_name("document-open-symbolic"); + row.add_suffix(&file_icon); + + let title = title.to_owned(); + let dialog_cb = clone!( + #[weak] + row, + move |res: Result| { + if let Ok(file) = res { + if let Some(path) = file.path() { + row.set_subtitle(&path.display().to_string()); + cb(path); } } - ); - row - + } + ); + row.connect_activated(move |_| { + let file_dialog = gtk::FileDialog::builder().title(&title).modal(true).build(); + let dialog_cb = dialog_cb.clone(); + match selection { + FileRowSelection::File => { + file_dialog.open(None::<>k::Window>, None::<&gio::Cancellable>, dialog_cb); + } + FileRowSelection::Folder => { + file_dialog.select_folder( + None::<>k::Window>, + None::<&gio::Cancellable>, + dialog_cb, + ); + } + } + }); + row } - pub fn update_create_args(&self) -> Result<(), Error> { + pub async fn extract_create_args(&self) -> Result { let imp = self.imp(); let image = imp .image_row @@ -393,32 +394,45 @@ impl CreateDistroboxDialog { .iter() .filter_map(|entry| { if !entry.text().is_empty() { - Some(entry.text().to_string()) + match entry.text().parse::() { + Ok(volume) => Some(Ok(volume)), + Err(e) => Some(Err(e)), + } } else { None } }) - .collect::>(); + .collect::, _>>()?; let name = CreateArgName::new(&imp.name_row.text())?; + dbg!(&self.home_folder()); let create_args = CreateArgs { name, image: image.to_string(), nvidia: imp.nvidia_row.is_active(), - home_path: self.home_folder(), + home_path: if let Some(home) = self.home_folder() { + Some( + self.root_store() + .resolve_host_path(&home) + .await + .map_err(|e| Error::InvalidField("home".to_string(), e.to_string()))?, + ) + } else { + None + }, init: imp.init_row.is_active(), volumes, }; + dbg!(&create_args); - self.imp().current_create_args.replace(create_args); - Ok(()) + Ok(create_args) } pub fn build_volumes_group(&self) -> adw::PreferencesGroup { let volumes_group = adw::PreferencesGroup::new(); volumes_group.set_title("Volumes"); - volumes_group.set_description(Some("Specify volumes in the format 'dest_dir:source_dir'")); + volumes_group.set_description(Some("Specify volumes in the format 'host_path:container_path'")); let add_volume_button = adw::ButtonRow::builder().title("Add Volume").build(); add_volume_button.connect_activated(clone!( @@ -462,10 +476,13 @@ impl CreateDistroboxDialog { volumes_group } - fn update_errors(&self, res: &Result<(), distrobox::Error>) { + fn update_errors(&self, res: &Result) { let imp = self.imp(); imp.name_row.remove_css_class("error"); imp.name_row.set_tooltip_text(None); + if let Err(ref e) = res { + error!(error = %e, "CreateDistroboxDialog: update_errors"); + } match res { Err(distrobox::Error::InvalidField(field, msg)) if field == "name" => { imp.name_row.add_css_class("error"); diff --git a/src/distrobox/mod.rs b/src/distrobox/mod.rs index 57f512f..e4fc604 100644 --- a/src/distrobox/mod.rs +++ b/src/distrobox/mod.rs @@ -183,7 +183,66 @@ pub struct CreateArgs { pub home_path: Option, pub image: String, pub name: CreateArgName, - pub volumes: Vec, + pub volumes: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum VolumeMode { + ReadOnly, +} + +impl std::fmt::Display for VolumeMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VolumeMode::ReadOnly => write!(f, "ro"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Volume { + pub host_path: String, + pub container_path: String, + pub mode: Option, +} + +impl FromStr for Volume { + type Err = Error; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + match parts.as_slice() { + [host] => Ok(Volume { + host_path: host.to_string(), + container_path: host.to_string(), + mode: None, + }), + [host, target] => Ok(Volume { + host_path: host.to_string(), + container_path: target.to_string(), + mode: None, + }), + [host, target, "ro"] => Ok(Volume { + host_path: host.to_string(), + container_path: target.to_string(), + mode: Some(VolumeMode::ReadOnly), + }), + _ => Err(Error::InvalidField( + "volume".into(), + format!("Invalid volume descriptor: {}", s), + )), + } + } +} + +impl std::fmt::Display for Volume { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.host_path, self.container_path)?; + if let Some(mode) = &self.mode { + write!(f, ":{}", mode)?; + } + Ok(()) + } } #[derive(thiserror::Error, Debug)] @@ -206,6 +265,9 @@ pub enum Error { command: String, stderr: String, }, + + #[error("failed to resolve host path: {0}. getfattr may not be installed on the host")] + ResolveHostPath(String), } fn dbcmd() -> Command { @@ -685,7 +747,7 @@ impl Distrobox { cmd.arg("--home").arg(home_path); } for volume in args.volumes { - cmd.arg("--volume").arg(volume); + cmd.arg("--volume").arg(volume.to_string()); } self.cmd_spawn(cmd) } @@ -929,12 +991,14 @@ Categories=Utility;Network; init: true, nvidia: true, home_path: Some("/home/me".into()), - volumes: vec!["/mnt/sdb1".into(), "/mnt/sdb4".into()], + volumes: vec![ + Volume::from_str("/mnt/sdb1:/mnt/sdb1")?, + Volume::from_str("/mnt/sdb4:/mnt/sdb4:ro")?, + ], ..Default::default() }; - - block_on(db.create(args))?; - let expected = "\"distrobox\" [\"create\", \"--yes\", \"--image\", \"docker.io/library/ubuntu:latest\", \"--init\", \"--nvidia\", \"--home\", \"/home/me\", \"--volume\", \"/mnt/sdb1\", \"--volume\", \"/mnt/sdb4\"]"; + smol::block_on(db.create(args))?; + let expected = "\"distrobox\" [\"create\", \"--yes\", \"--image\", \"docker.io/library/ubuntu:latest\", \"--init\", \"--nvidia\", \"--home\", \"/home/me\", \"--volume\", \"/mnt/sdb1:/mnt/sdb1\", \"--volume\", \"/mnt/sdb4:/mnt/sdb4:ro\"]"; assert_eq!(output_tracker.items()[0], expected); Ok(()) } diff --git a/src/store/root_store.rs b/src/store/root_store.rs index 044c5f6..76213ad 100644 --- a/src/store/root_store.rs +++ b/src/store/root_store.rs @@ -8,6 +8,7 @@ use gtk::prelude::*; use gtk::{gio, glib}; use std::cell::RefCell; use std::default; +use std::path::PathBuf; use std::rc::Rc; use std::time::Duration; use tracing::debug; @@ -15,7 +16,7 @@ use tracing::error; use tracing::info; use crate::container::Container; -use crate::distrobox::Command; +use crate::distrobox::{self, wrap_capture_cmd, Command}; use crate::distrobox::CreateArgs; use crate::distrobox::Distrobox; use crate::distrobox::Status; @@ -70,7 +71,9 @@ mod imp { Self { containers: gio::ListStore::new::(), command_runner: OnceCell::new(), - terminal_repository: RefCell::new(TerminalRepository::new(Rc::new(NullCommandRunner::default()))), + terminal_repository: RefCell::new(TerminalRepository::new(Rc::new( + NullCommandRunner::default(), + ))), selected_container: Default::default(), current_view: Default::default(), current_dialog: Default::default(), @@ -299,7 +302,7 @@ impl RootStore { .terminal_repository .borrow() .terminal_by_name(&name_or_program); - + if let Some(terminal) = by_name { Some(terminal) } else if let Some(terminal) = self @@ -376,6 +379,38 @@ impl RootStore { } }); } + + pub async fn resolve_host_path(&self, path: &str) -> Result { + let mut cmd = Command::new_with_args( + "getfattr", + [ + "-n", + "user.document-portal.host-path", + "--only-values", + path, + ], + ); + wrap_capture_cmd(&mut cmd); + let output = self.command_runner() + .output(cmd) + .await + .map_err(|e| distrobox::Error::ResolveHostPath(e.to_string())); + + + let is_from_sandbox = path.starts_with("/run/user"); + + // If the path is not from a flatpak sandbox, we assume it's a regular path, so we can skip the getfattr command error. + // If the command was successful, but for some reason the output is empty, we also return the path as is. + let stdout = if (output.is_err() && !is_from_sandbox) || output.as_ref().map_or(false, |o| o.stdout.is_empty()) { + return Ok(path.to_string()); + } else { + output?.stdout + }; + + Ok(String::from_utf8(stdout) + .map_err(|e| distrobox::Error::ParseOutput(e.to_string()))? + .trim().to_string()) + } } impl Default for RootStore {