Skip to main content

btrfs_cli/property/
set.rs

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