🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
//! Miejsca (XDG) i urządzenia (montowania) do panelu bocznego.
//!
//! Lista montowań czytana jest z `/proc/self/mountinfo`, a rozmiary przez `statvfs`.
//! Montowanie/odmontowanie idzie przez `udisksctl` (UDisks2 po D-Busie, z polkit —
//! hasło pyta `luna-polkit`). Docelowo podmienimy to na bezpośrednie sygnały D-Bus;
//! interfejs funkcji zostaje ten sam.
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use libluna_i18n::tr;
#[derive(Debug, Clone)]
pub struct Place {
pub label: String,
pub path: PathBuf,
}
impl Place {
fn new(label: &str, path: PathBuf) -> Self {
Self {
label: tr(label),
path,
}
}
}
/// Katalogi użytkownika z `~/.config/user-dirs.dirs` (z wartościami domyślnymi).
pub fn user_dirs() -> BTreeMap<String, PathBuf> {
let home = home();
let mut map = BTreeMap::new();
for key in [
"DESKTOP",
"DOCUMENTS",
"DOWNLOAD",
"PICTURES",
"MUSIC",
"VIDEOS",
"TEMPLATES",
"PUBLICSHARE",
] {
map.insert(key.to_owned(), home.clone());
}
let config = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".config"))
.join("user-dirs.dirs");
if let Ok(text) = fs::read_to_string(config) {
for line in text.lines() {
let line = line.trim();
if line.starts_with('#') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let Some(key) = key.trim().strip_prefix("XDG_") else {
continue;
};
let Some(key) = key.strip_suffix("_DIR") else {
continue;
};
let value = value.trim().trim_matches('"');
let path = value.replace("$HOME", &home.display().to_string());
if !path.is_empty() {
map.insert(key.to_owned(), PathBuf::from(path));
}
}
}
map
}
pub fn home() -> PathBuf {
env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/"))
}
/// Miejsca w panelu bocznym (tylko istniejące katalogi).
pub fn places() -> Vec<Place> {
let dirs = user_dirs();
let mut out = vec![Place::new("Home", home())];
for (key, label) in [
("DESKTOP", "Desktop"),
("DOCUMENTS", "Documents"),
("DOWNLOAD", "Downloads"),
("PICTURES", "Pictures"),
("MUSIC", "Music"),
("VIDEOS", "Videos"),
] {
if let Some(path) = dirs.get(key) {
if path.is_dir() && path != &home() {
out.push(Place::new(label, path.clone()));
}
}
}
out.push(Place::new("File System", PathBuf::from("/")));
out
}
/// Zamontowane urządzenie widoczne w panelu bocznym.
#[derive(Debug, Clone)]
pub struct Mount {
pub dev: String,
pub label: String,
pub mount: PathBuf,
pub fs: String,
pub total: u64,
pub free: u64,
}
/// Lista montowań „użytkowych” (pomija pseudosystemy i montowania systemowe).
pub fn mounts() -> Vec<Mount> {
let Ok(text) = fs::read_to_string("/proc/self/mountinfo") else {
return Vec::new();
};
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
for line in text.lines() {
let Some(m) = parse_mountinfo(line) else {
continue;
};
if !user_visible_mount(&m.mount, &m.fs) {
continue;
}
if !seen.insert(m.mount.clone()) {
continue;
}
let (total, free) = statvfs(&m.mount).unwrap_or((0, 0));
out.push(Mount {
label: mount_label(&m.dev).unwrap_or_else(|| {
m.mount
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| m.dev.clone())
}),
dev: m.dev,
mount: m.mount,
fs: m.fs,
total,
free,
});
}
out.sort_by(|a, b| a.label.to_lowercase().cmp(&b.label.to_lowercase()));
out
}
struct MountInfo {
dev: String,
mount: PathBuf,
fs: String,
}
/// Parsuje jedną linię `/proc/self/mountinfo`:
/// `36 35 98:0 /root /mnt rw - ext4 /dev/sda1 rw`.
fn parse_mountinfo(line: &str) -> Option<MountInfo> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 7 {
return None;
}
let mount = PathBuf::from(unescape(parts[4]));
let sep = parts.iter().position(|p| *p == "-")?;
let fs = parts.get(sep + 1)?.to_string();
let dev = parts.get(sep + 2).unwrap_or(&"").to_string();
Some(MountInfo { dev, mount, fs })
}
fn user_visible_mount(mount: &Path, fs: &str) -> bool {
const FS: [&str; 18] = [
"ext2", "ext3", "ext4", "btrfs", "xfs", "f2fs", "vfat", "exfat", "ntfs", "ntfs3",
"fuseblk", "iso9660", "udf", "hfsplus", "hfs", "msdos", "jfs", "reiserfs",
];
if !FS.contains(&fs) {
return false;
}
const SYSTEM: [&str; 9] = [
"/",
"/boot",
"/boot/efi",
"/home",
"/tmp",
"/var",
"/usr",
"/opt",
"/srv",
];
if SYSTEM.iter().any(|s| mount == Path::new(s)) {
return false;
}
true
}
/// Odkodowuje oktalne sekwencje z mountinfo (`\040` = spacja).
fn unescape(text: &str) -> String {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 3 < bytes.len() {
if let Ok(v) = u8::from_str_radix(&text[i + 1..i + 4], 8) {
out.push(v);
i += 4;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
/// Etykieta wolumenu z `/dev/disk/by-label` (np. „PENDRIVE”).
pub fn mount_label(dev: &str) -> Option<String> {
let dir = Path::new("/dev/disk/by-label");
let target = fs::canonicalize(dev).ok()?;
for entry in fs::read_dir(dir).ok()?.flatten() {
if fs::canonicalize(entry.path()).ok().as_ref() == Some(&target) {
return Some(entry.file_name().to_string_lossy().replace("\\x20", " "));
}
}
None
}
/// `(rozmiar, wolne)` w bajtach.
pub fn statvfs(path: &Path) -> Option<(u64, u64)> {
let st = rustix::fs::statvfs(path).ok()?;
let unit = if st.f_frsize != 0 {
st.f_frsize
} else {
st.f_bsize
};
Some((st.f_blocks * unit, st.f_bavail * unit))
}
/// Montuje urządzenie przez UDisks2 (polkit pyta o hasło, gdy trzeba).
pub fn mount_device(dev: &str) {
spawn_udisks(&["mount", "-b", dev]);
}
/// Odmontowuje urządzenie przez UDisks2.
pub fn unmount_device(dev: &str) {
spawn_udisks(&["unmount", "-b", dev]);
}
fn spawn_udisks(args: &[&str]) {
if let Err(e) = Command::new("udisksctl").args(args).spawn() {
eprintln!("luna-files: udisksctl {:?}: {e}", args);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mountinfo_line_is_parsed() {
let line =
"36 35 98:0 /mnt1 /run/media/patryk/PEN\\040DRIVE rw,noatime - vfat /dev/sdb1 rw";
let m = parse_mountinfo(line).unwrap();
assert_eq!(m.mount, PathBuf::from("/run/media/patryk/PEN DRIVE"));
assert_eq!(m.fs, "vfat");
assert_eq!(m.dev, "/dev/sdb1");
}
#[test]
fn system_mounts_are_hidden() {
assert!(user_visible_mount(Path::new("/run/media/x/USB"), "vfat"));
assert!(!user_visible_mount(Path::new("/"), "ext4"));
assert!(!user_visible_mount(Path::new("/home"), "ext4"));
assert!(!user_visible_mount(Path::new("/mnt/x"), "proc"));
}
#[test]
fn octal_sequences_are_unescaped() {
assert_eq!(unescape("a\\040b\\134c"), "a b\\c");
}
}