use crate::domain::error::Result;
use crate::domain::resource::{Ensure, FileResource, Resource, ResourceProvider, ResourceState};
use async_trait::async_trait;
use nix::unistd::{Group, User, chown};
use std::os::unix::fs::PermissionsExt;
use tokio::fs;
use tokio::io::AsyncWriteExt;
pub struct FsAdapter;
#[async_trait]
impl ResourceProvider for FsAdapter {
fn can_handle(&self, resource: &Resource) -> bool {
matches!(resource, Resource::File(_) | Resource::Directory(_))
}
async fn get_state(&self, resource: &Resource, full: bool) -> Result<ResourceState> {
match resource {
Resource::File(file) => {
let physical_exists = file.path.exists() && !file.path.is_dir();
if full && physical_exists {
let content = fs::read(&file.path).await.ok();
Ok(ResourceState::Full {
ensure: Ensure::Present,
content,
})
} else {
let ensure = self.get_file_ensure(file).await?;
Ok(ResourceState::Ensure(ensure))
}
}
Resource::Directory(dir) => {
let ensure = if dir.path.exists() && dir.path.is_dir() {
Ensure::Present
} else {
Ensure::Absent
};
Ok(ResourceState::Ensure(ensure))
}
Resource::Exec(_) | Resource::Package(_) | Resource::Meta(_) => {
Ok(ResourceState::Ensure(Ensure::Present))
}
}
}
async fn apply(&self, resource: &Resource) -> Result<()> {
match resource {
Resource::File(file) => self.apply_file(file).await,
Resource::Directory(dir) => self.apply_directory(dir).await,
Resource::Exec(_) | Resource::Package(_) | Resource::Meta(_) => Ok(()),
}
}
}
impl FsAdapter {
async fn apply_directory(
&self,
dir: &crate::domain::resource::DirectoryResource,
) -> Result<()> {
if dir.ensure == Ensure::Present {
if !dir.path.exists() {
fs::create_dir_all(&dir.path)
.await
.map_err(|e| anyhow::anyhow!("Failed to create directory: {}", e))?;
}
self.apply_metadata(&dir.path, &dir.owner, &dir.group, &dir.mode)
.await?;
} else if dir.path.exists() {
fs::remove_dir_all(&dir.path)
.await
.map_err(|e| anyhow::anyhow!("Failed to remove directory: {}", e))?;
}
Ok(())
}
async fn apply_metadata(
&self,
path: &std::path::Path,
owner: &Option<String>,
group: &Option<String>,
mode: &Option<String>,
) -> Result<()> {
if let Some(mode_str) = mode {
let mode_val = u32::from_str_radix(mode_str, 8)
.map_err(|e| anyhow::anyhow!("Invalid mode octal string '{}': {}", mode_str, e))?;
let mut perms = fs::metadata(path)
.await
.map_err(|e| anyhow::anyhow!("Failed to get metadata: {}", e))?
.permissions();
perms.set_mode(mode_val);
fs::set_permissions(path, perms)
.await
.map_err(|e| anyhow::anyhow!("Failed to set permissions: {}", e))?;
}
let uid = Self::resolve_user(owner).await?;
let gid = Self::resolve_group(group).await?;
if uid.is_some() || gid.is_some() {
chown(path, uid, gid).map_err(|e| anyhow::anyhow!("Failed to chown: {}", e))?;
}
Ok(())
}
async fn resolve_user(owner: &Option<String>) -> Result<Option<nix::unistd::Uid>> {
if let Some(owner_name) = owner {
let user = User::from_name(owner_name)
.map_err(|e| anyhow::anyhow!("Failed to resolve user '{}': {}", owner_name, e))?
.ok_or_else(|| anyhow::anyhow!("User '{}' not found", owner_name))?;
Ok(Some(user.uid))
} else {
Ok(None)
}
}
async fn resolve_group(group: &Option<String>) -> Result<Option<nix::unistd::Gid>> {
if let Some(group_name) = group {
let grp = Group::from_name(group_name)
.map_err(|e| anyhow::anyhow!("Failed to resolve group '{}': {}", group_name, e))?
.ok_or_else(|| anyhow::anyhow!("Group '{}' not found", group_name))?;
Ok(Some(grp.gid))
} else {
Ok(None)
}
}
async fn get_file_ensure(&self, file: &FileResource) -> Result<Ensure> {
if !file.path.exists() {
return Ok(Ensure::Absent);
}
if file.path.is_dir() {
return Ok(Ensure::Absent);
}
if let Some(expected_content) = &file.content {
let actual_content = fs::read_to_string(&file.path)
.await
.map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
if &actual_content != expected_content {
return Ok(Ensure::Absent);
}
}
Ok(Ensure::Present)
}
async fn apply_file(&self, file: &FileResource) -> Result<()> {
match file.ensure {
Ensure::Present => {
if let Some(parent) = file.path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)
.await
.map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?;
}
{
let mut f = fs::File::create(&file.path)
.await
.map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?;
if let Some(content) = &file.content {
f.write_all(content.as_bytes())
.await
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
}
f.flush()
.await
.map_err(|e| anyhow::anyhow!("Failed to flush file: {}", e))?;
}
self.apply_metadata(&file.path, &file.owner, &file.group, &file.mode)
.await?;
tracing::debug!(path = %file.path.display(), "File ensured present");
}
Ensure::Absent => {
if file.path.exists() {
fs::remove_file(&file.path)
.await
.map_err(|e| anyhow::anyhow!("Failed to remove file: {}", e))?;
tracing::debug!(path = %file.path.display(), "File ensured absent");
}
}
}
Ok(())
}
}