use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub struct Paths;
impl Paths {
pub fn config_dir() -> PathBuf {
if let Ok(d) = std::env::var("FILAMENT_CONFIG_DIR") {
return PathBuf::from(d);
}
Self::platform_config_dir()
}
fn platform_config_dir() -> PathBuf {
if let Some(proj) = directories::ProjectDirs::from("", "", "filament") {
return proj.config_dir().to_path_buf();
}
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".config").join("filament")
}
pub fn config_path(relative: impl AsRef<Path>) -> PathBuf {
Self::config_dir().join(relative)
}
pub fn migrate_legacy() {
let legacy = PathBuf::from(".config/filament");
if !legacy.is_dir() {
return;
}
let target = Self::config_dir();
if target == legacy || target.exists() {
return;
}
let _ = std::fs::create_dir_all(&target);
if let Ok(entries) = std::fs::read_dir(&legacy) {
for e in entries.flatten() {
let dest = target.join(e.file_name());
let _ = std::fs::copy(e.path(), &dest);
}
}
}
}
pub struct SecretFile;
impl SecretFile {
pub fn write(path: impl AsRef<Path>, data: &[u8]) -> io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
SecretFile::write_raw(path, data)?;
SecretFile::restrict(path)?;
Ok(())
}
pub fn write_str(path: impl AsRef<Path>, s: &str) -> io::Result<()> {
SecretFile::write(path, s.as_bytes())
}
pub fn restrict(path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
}
#[cfg(windows)]
{
restrict_dacl(path)?;
}
Ok(())
}
fn write_raw(path: &Path, data: &[u8]) -> io::Result<()> {
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(path)?;
f.write_all(data)?;
f.sync_all()?;
Ok(())
}
}
#[cfg(windows)]
fn restrict_dacl(path: &Path) -> io::Result<()> {
let user = std::env::var("USERNAME").unwrap_or_default();
if user.is_empty() {
return Ok(());
}
let path_str = path.display().to_string();
let _ = std::process::Command::new("icacls")
.args([&path_str, "/inheritance:r", "/grant:r", &format!("{user}:(F)"), "/Q"])
.output();
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceHost {
Systemd,
Launchd,
WindowsService,
None,
}
impl ServiceHost {
pub fn detect() -> Self {
#[cfg(target_os = "linux")]
{
if Self::has_systemd() {
return ServiceHost::Systemd;
}
ServiceHost::None
}
#[cfg(target_os = "macos")]
{
ServiceHost::Launchd
}
#[cfg(target_os = "windows")]
{
ServiceHost::WindowsService
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
ServiceHost::None
}
}
pub fn supports_install(&self) -> bool {
matches!(self, ServiceHost::Systemd)
}
pub fn install_instructions(&self) -> &'static str {
match self {
ServiceHost::Launchd => "On macOS, create a LaunchAgent plist in ~/Library/LaunchAgents/ and load it with launchctl.",
ServiceHost::WindowsService => "On Windows, create a Scheduled Task (trigger: at logon) or register a Service with sc.exe.",
ServiceHost::None => "No service manager detected. Start filament with `filament up` in a terminal, or configure your init system manually.",
ServiceHost::Systemd => "",
}
}
#[cfg(target_os = "linux")]
fn has_systemd() -> bool {
std::path::Path::new("/run/systemd/system").is_dir()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallSource {
Homebrew,
Winget,
Scoop,
Cargo,
SelfInstalled,
}
impl InstallSource {
pub fn detect() -> Self {
let path = match std::env::current_exe() {
Ok(p) => match p.canonicalize() {
Ok(c) => c,
Err(_) => p,
},
Err(_) => return InstallSource::SelfInstalled,
};
Self::classify(&path)
}
fn classify(path: &Path) -> Self {
let s = path.to_string_lossy().to_lowercase();
if s.contains("/cellar/") || s.contains("/homebrew/") || s.contains("/linuxbrew/") {
return InstallSource::Homebrew;
}
if s.contains("\\microsoft\\winget\\") || s.contains("/microsoft/winget/") {
return InstallSource::Winget;
}
if s.contains("\\scoop\\apps\\") || s.contains("/scoop/apps/") {
return InstallSource::Scoop;
}
if s.contains("/.cargo/bin") || s.contains("\\.cargo\\bin") {
return InstallSource::Cargo;
}
InstallSource::SelfInstalled
}
pub fn upgrade_hint(&self) -> &'static str {
match self {
InstallSource::Homebrew => "brew upgrade filament",
InstallSource::Winget => "winget upgrade Abdk4Moura.Filament",
InstallSource::Scoop => "scoop update filament",
InstallSource::Cargo => "cargo install filament-cli",
InstallSource::SelfInstalled => "",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_returns_a_valid_variant() {
let host = ServiceHost::detect();
assert!(
matches!(
host,
ServiceHost::Systemd
| ServiceHost::Launchd
| ServiceHost::WindowsService
| ServiceHost::None
),
"detect returned a valid variant: {host:?}"
);
}
#[test]
fn systemd_supports_install_others_do_not() {
assert!(ServiceHost::Systemd.supports_install());
assert!(!ServiceHost::Launchd.supports_install());
assert!(!ServiceHost::WindowsService.supports_install());
assert!(!ServiceHost::None.supports_install());
}
#[test]
fn install_instructions_non_empty_when_no_backend() {
assert!(!ServiceHost::Launchd.install_instructions().is_empty());
assert!(!ServiceHost::WindowsService.install_instructions().is_empty());
assert!(!ServiceHost::None.install_instructions().is_empty());
assert!(ServiceHost::Systemd.install_instructions().is_empty());
}
#[test]
fn install_source_classify_brew() {
let p = Path::new("/opt/homebrew/Cellar/filament/0.4.1/bin/filament");
assert_eq!(InstallSource::classify(p), InstallSource::Homebrew);
let p2 = Path::new("/home/linuxbrew/.linuxbrew/bin/filament");
assert_eq!(InstallSource::classify(p2), InstallSource::Homebrew);
let p3 = Path::new("/usr/local/Cellar/filament/0.3.1/bin/filament");
assert_eq!(InstallSource::classify(p3), InstallSource::Homebrew);
}
#[test]
fn install_source_classify_winget() {
let p = Path::new("C:\\Users\\kabir\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Abdk4Moura.Filament_filament\\filament.exe");
assert_eq!(InstallSource::classify(p), InstallSource::Winget);
}
#[test]
fn install_source_classify_scoop() {
let p = Path::new("C:\\Users\\kabir\\scoop\\apps\\filament\\0.4.1\\filament.exe");
assert_eq!(InstallSource::classify(p), InstallSource::Scoop);
}
#[test]
fn install_source_classify_cargo() {
let p = Path::new("/home/kabir/.cargo/bin/filament");
assert_eq!(InstallSource::classify(p), InstallSource::Cargo);
}
#[test]
fn install_source_classify_self_installed() {
let p = Path::new("/home/kabir/.local/bin/filament");
assert_eq!(InstallSource::classify(p), InstallSource::SelfInstalled);
}
#[test]
fn install_source_upgrade_hints() {
assert_eq!(InstallSource::Homebrew.upgrade_hint(), "brew upgrade filament");
assert_eq!(InstallSource::Winget.upgrade_hint(), "winget upgrade Abdk4Moura.Filament");
assert_eq!(InstallSource::Cargo.upgrade_hint(), "cargo install filament-cli");
assert_eq!(InstallSource::SelfInstalled.upgrade_hint(), "");
}
}