use super::{PropertyObjectType, detect_object_types};
use crate::{RunContext, Runnable, util::open_path};
use anyhow::{Context, Result, anyhow, bail};
use btrfs_uapi::{
filesystem::label_set,
subvolume::{SubvolumeFlags, subvolume_flags_get, subvolume_flags_set},
};
use clap::Parser;
use std::{
ffi::CString,
fs::File,
os::unix::io::AsFd,
path::{Path, PathBuf},
};
#[derive(Parser, Debug)]
pub struct PropertySetCommand {
pub object: PathBuf,
pub name: String,
pub value: String,
#[clap(short = 't', long = "type")]
pub object_type: Option<PropertyObjectType>,
#[clap(short = 'f', long)]
pub force: bool,
}
impl Runnable for PropertySetCommand {
fn run(&self, _ctx: &RunContext) -> Result<()> {
let file = open_path(&self.object)?;
let detected_types = detect_object_types(&self.object);
let target_type = if let Some(t) = self.object_type {
t
} else {
if detected_types.len() > 1 {
bail!(
"object type is ambiguous, please use option -t (detected: {detected_types:?})"
);
}
detected_types
.first()
.copied()
.ok_or_else(|| anyhow!("object is not a btrfs object"))?
};
set_property(
&file,
target_type,
&self.name,
&self.value,
self.force,
&self.object,
)?;
Ok(())
}
}
fn set_property(
file: &File,
obj_type: PropertyObjectType,
name: &str,
value: &str,
force: bool,
path: &Path,
) -> Result<()> {
match (obj_type, name) {
(PropertyObjectType::Subvol, "ro") => {
set_readonly_property(file, value, force, path)?;
}
(
PropertyObjectType::Filesystem | PropertyObjectType::Device,
"label",
) => {
let cstring = CString::new(value.as_bytes())
.context("label must not contain null bytes")?;
label_set(file.as_fd(), &cstring).with_context(|| {
format!("failed to set label for '{}'", path.display())
})?;
}
(PropertyObjectType::Inode, "compression") => {
set_compression_property(file, value, path)?;
}
_ => {
bail!(
"property '{name}' is not applicable to object type {obj_type:?}"
);
}
}
Ok(())
}
fn set_readonly_property(
file: &File,
value: &str,
force: bool,
path: &Path,
) -> Result<()> {
let new_readonly = match value {
"true" => true,
"false" => false,
_ => bail!("invalid value for property: {value}"),
};
let current_flags =
subvolume_flags_get(file.as_fd()).with_context(|| {
format!("failed to get flags for '{}'", path.display())
})?;
let is_readonly = current_flags.contains(SubvolumeFlags::RDONLY);
if is_readonly == new_readonly {
return Ok(());
}
if is_readonly && !new_readonly {
let info = btrfs_uapi::subvolume::subvolume_info(file.as_fd())
.with_context(|| {
format!("failed to get subvolume info for '{}'", path.display())
})?;
if !info.received_uuid.is_nil() && !force {
bail!(
"cannot flip ro->rw with received_uuid set, use force option -f if you really want to unset the read-only status. \
The value of received_uuid is used for incremental send, consider making a snapshot instead."
);
}
}
let mut new_flags = current_flags;
if new_readonly {
new_flags |= SubvolumeFlags::RDONLY;
} else {
new_flags &= !SubvolumeFlags::RDONLY;
}
subvolume_flags_set(file.as_fd(), new_flags).with_context(|| {
format!("failed to set flags for '{}'", path.display())
})?;
if is_readonly && !new_readonly && force {
let info = btrfs_uapi::subvolume::subvolume_info(file.as_fd()).ok();
if let Some(info) = info
&& !info.received_uuid.is_nil()
{
eprintln!(
"clearing received_uuid (was {})",
info.received_uuid.as_hyphenated()
);
if let Err(e) = btrfs_uapi::send_receive::received_subvol_set(
file.as_fd(),
&uuid::Uuid::nil(),
0,
) {
eprintln!(
"WARNING: failed to clear received_uuid on '{}': {e}",
path.display()
);
}
}
}
Ok(())
}
fn set_compression_property(
file: &File,
value: &str,
path: &Path,
) -> Result<()> {
use nix::libc::fsetxattr;
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let xattr_name = "btrfs.compression\0";
let result = unsafe {
fsetxattr(
fd,
xattr_name.as_ptr().cast(),
value.as_ptr().cast(),
value.len(),
0,
)
};
if result < 0 {
return Err(anyhow::anyhow!(
"failed to set compression for '{}': {}",
path.display(),
nix::errno::Errno::last()
));
}
Ok(())
}