Skip to main content

btrfs_cli/property/
set.rs

1use super::{PropertyObjectType, detect_object_types};
2use crate::{Format, Runnable, util::open_path};
3use anyhow::{Context, Result, anyhow, bail};
4use btrfs_uapi::{
5    filesystem::label_set,
6    subvolume::{SubvolumeFlags, subvolume_flags_get, subvolume_flags_set},
7};
8use clap::Parser;
9use std::{
10    ffi::CString,
11    fs::File,
12    os::unix::io::AsFd,
13    path::{Path, PathBuf},
14};
15
16/// Set a property on a btrfs object
17#[derive(Parser, Debug)]
18pub struct PropertySetCommand {
19    /// Path to the btrfs object
20    pub object: PathBuf,
21
22    /// Name of the property to set
23    pub name: String,
24
25    /// Value to assign to the property
26    pub value: String,
27
28    /// Object type (inode, subvol, filesystem, device)
29    #[clap(short = 't', long = "type")]
30    pub object_type: Option<PropertyObjectType>,
31
32    /// Force the change
33    #[clap(short = 'f', long)]
34    pub force: bool,
35}
36
37impl Runnable for PropertySetCommand {
38    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
39        let file = open_path(&self.object)?;
40
41        // Detect object type if not specified
42        let detected_types = detect_object_types(&self.object);
43        let target_type = match self.object_type {
44            Some(t) => t,
45            None => {
46                // If ambiguous, require the user to specify
47                if detected_types.len() > 1 {
48                    bail!(
49                        "object type is ambiguous, please use option -t (detected: {:?})",
50                        detected_types
51                    );
52                }
53                detected_types
54                    .first()
55                    .copied()
56                    .ok_or_else(|| anyhow!("object is not a btrfs object"))?
57            }
58        };
59
60        set_property(
61            &file,
62            target_type,
63            &self.name,
64            &self.value,
65            self.force,
66            &self.object,
67        )?;
68
69        Ok(())
70    }
71}
72
73fn set_property(
74    file: &File,
75    obj_type: PropertyObjectType,
76    name: &str,
77    value: &str,
78    force: bool,
79    path: &Path,
80) -> Result<()> {
81    match (obj_type, name) {
82        (PropertyObjectType::Subvol, "ro") => {
83            set_readonly_property(file, value, force, path)?;
84        }
85        (PropertyObjectType::Filesystem, "label")
86        | (PropertyObjectType::Device, "label") => {
87            let cstring = CString::new(value.as_bytes())
88                .context("label must not contain null bytes")?;
89            label_set(file.as_fd(), &cstring).with_context(|| {
90                format!("failed to set label for '{}'", path.display())
91            })?;
92        }
93        (PropertyObjectType::Inode, "compression") => {
94            set_compression_property(file, value, path)?;
95        }
96        _ => {
97            bail!(
98                "property '{}' is not applicable to object type {:?}",
99                name,
100                obj_type
101            );
102        }
103    }
104
105    Ok(())
106}
107
108fn set_readonly_property(
109    file: &File,
110    value: &str,
111    force: bool,
112    path: &Path,
113) -> Result<()> {
114    let new_readonly = match value {
115        "true" => true,
116        "false" => false,
117        _ => bail!("invalid value for property: {}", value),
118    };
119
120    let current_flags =
121        subvolume_flags_get(file.as_fd()).with_context(|| {
122            format!("failed to get flags for '{}'", path.display())
123        })?;
124    let is_readonly = current_flags.contains(SubvolumeFlags::RDONLY);
125
126    // No change if already in desired state
127    if is_readonly == new_readonly {
128        return Ok(());
129    }
130
131    // If going from ro to rw, check for received_uuid
132    if is_readonly && !new_readonly {
133        let info = btrfs_uapi::subvolume::subvolume_info(file.as_fd())
134            .with_context(|| {
135                format!("failed to get subvolume info for '{}'", path.display())
136            })?;
137
138        if !info.received_uuid.is_nil() && !force {
139            bail!(
140                "cannot flip ro->rw with received_uuid set, use force option -f if you really want to unset the read-only status. \
141                     The value of received_uuid is used for incremental send, consider making a snapshot instead."
142            );
143        }
144    }
145
146    let mut new_flags = current_flags;
147    if new_readonly {
148        new_flags |= SubvolumeFlags::RDONLY;
149    } else {
150        new_flags &= !SubvolumeFlags::RDONLY;
151    }
152
153    subvolume_flags_set(file.as_fd(), new_flags).with_context(|| {
154        format!("failed to set flags for '{}'", path.display())
155    })?;
156
157    // Clear received_uuid after flipping ro→rw with force.  This must
158    // happen after the flag change (the kernel rejects SET_RECEIVED_SUBVOL
159    // on a read-only subvolume). If it fails, warn but don't error —
160    // matching the C reference behaviour.
161    if is_readonly && !new_readonly && force {
162        let info = btrfs_uapi::subvolume::subvolume_info(file.as_fd()).ok();
163        if let Some(info) = info
164            && !info.received_uuid.is_nil()
165        {
166            eprintln!(
167                "clearing received_uuid (was {})",
168                info.received_uuid.as_hyphenated()
169            );
170            if let Err(e) = btrfs_uapi::send_receive::received_subvol_set(
171                file.as_fd(),
172                &uuid::Uuid::nil(),
173                0,
174            ) {
175                eprintln!(
176                    "WARNING: failed to clear received_uuid on '{}': {e}",
177                    path.display()
178                );
179            }
180        }
181    }
182
183    Ok(())
184}
185
186fn set_compression_property(
187    file: &File,
188    value: &str,
189    path: &Path,
190) -> Result<()> {
191    use nix::libc::fsetxattr;
192    use std::os::unix::io::AsRawFd;
193
194    let fd = file.as_raw_fd();
195    let xattr_name = "btrfs.compression\0";
196
197    // SAFETY: fsetxattr is safe to call with a valid fd and valid string pointers
198    let result = unsafe {
199        fsetxattr(
200            fd,
201            xattr_name.as_ptr() as *const i8,
202            value.as_ptr() as *const std::ffi::c_void,
203            value.len(),
204            0,
205        )
206    };
207
208    if result < 0 {
209        return Err(anyhow::anyhow!(
210            "failed to set compression for '{}': {}",
211            path.display(),
212            nix::errno::Errno::last()
213        ));
214    }
215
216    Ok(())
217}