fix(daemon): preserve file ownership on writes for cs2 addons

This commit is contained in:
hibna 2026-02-26 22:36:57 +00:00
parent c7d1627e18
commit 6b463c2b1a
1 changed files with 106 additions and 2 deletions

View File

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::debug;
@ -94,14 +94,24 @@ impl FileSystem {
/// Write file contents.
pub async fn write_file(&self, path: &str, data: &[u8]) -> Result<(), DaemonError> {
let resolved = self.resolve(path)?;
let owner = resolved
.parent()
.and_then(resolve_target_ownership);
// Ensure parent directory exists
if let Some(parent) = resolved.parent() {
fs::create_dir_all(parent).await.map_err(DaemonError::Io)?;
if let Some(ref owner) = owner {
apply_ownership_to_path_chain(parent, &owner)?;
}
}
debug!(path = %resolved.display(), "Writing file");
fs::write(&resolved, data).await.map_err(DaemonError::Io)
fs::write(&resolved, data).await.map_err(DaemonError::Io)?;
if let Some(ref owner) = owner {
apply_ownership(&resolved, owner.uid, owner.gid)?;
}
Ok(())
}
/// Delete files or directories.
@ -119,6 +129,100 @@ impl FileSystem {
}
}
#[derive(Clone, Debug)]
struct OwnershipTarget {
anchor: PathBuf,
uid: u32,
gid: u32,
}
#[cfg(unix)]
fn resolve_target_ownership(start: &Path) -> Option<OwnershipTarget> {
use std::os::unix::fs::MetadataExt;
let mut cursor = Some(start);
let mut fallback: Option<OwnershipTarget> = None;
while let Some(path) = cursor {
if let Ok(metadata) = std::fs::metadata(path) {
let candidate = OwnershipTarget {
anchor: path.to_path_buf(),
uid: metadata.uid(),
gid: metadata.gid(),
};
if fallback.is_none() {
fallback = Some(candidate.clone());
}
if candidate.uid != 0 || candidate.gid != 0 {
return Some(candidate);
}
}
cursor = path.parent();
}
fallback
}
#[cfg(not(unix))]
fn resolve_target_ownership(_start: &Path) -> Option<OwnershipTarget> {
None
}
#[cfg(unix)]
fn apply_ownership_to_path_chain(target: &Path, owner: &OwnershipTarget) -> Result<(), DaemonError> {
if !target.starts_with(&owner.anchor) {
return Ok(());
}
let mut current = owner.anchor.clone();
apply_ownership(&current, owner.uid, owner.gid)?;
let remainder = match target.strip_prefix(&owner.anchor) {
Ok(path) => path,
Err(_) => return Ok(()),
};
for component in remainder.components() {
current.push(component.as_os_str());
apply_ownership(&current, owner.uid, owner.gid)?;
}
Ok(())
}
#[cfg(not(unix))]
fn apply_ownership_to_path_chain(_target: &Path, _owner: &OwnershipTarget) -> Result<(), DaemonError> {
Ok(())
}
#[cfg(unix)]
fn apply_ownership(path: &Path, uid: u32, gid: u32) -> Result<(), DaemonError> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let bytes = path.as_os_str().as_bytes();
let c_path = CString::new(bytes).map_err(|err| {
DaemonError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid path for chown: {err}"),
))
})?;
let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
if result != 0 {
return Err(DaemonError::Io(std::io::Error::last_os_error()));
}
Ok(())
}
#[cfg(not(unix))]
fn apply_ownership(_path: &Path, _uid: u32, _gid: u32) -> Result<(), DaemonError> {
Ok(())
}
#[derive(Debug, Clone)]
pub struct FileEntry {
pub name: String,