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 = if let Some(t) = self.object_type {
44            t
45        } else {
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: {detected_types:?})"
50                );
51            }
52            detected_types
53                .first()
54                .copied()
55                .ok_or_else(|| anyhow!("object is not a btrfs object"))?
56        };
57
58        set_property(
59            &file,
60            target_type,
61            &self.name,
62            &self.value,
63            self.force,
64            &self.object,
65        )?;
66
67        Ok(())
68    }
69}
70
71fn set_property(
72    file: &File,
73    obj_type: PropertyObjectType,
74    name: &str,
75    value: &str,
76    force: bool,
77    path: &Path,
78) -> Result<()> {
79    match (obj_type, name) {
80        (PropertyObjectType::Subvol, "ro") => {
81            set_readonly_property(file, value, force, path)?;
82        }
83        (
84            PropertyObjectType::Filesystem | PropertyObjectType::Device,
85            "label",
86        ) => {
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 '{name}' is not applicable to object type {obj_type:?}"
99            );
100        }
101    }
102
103    Ok(())
104}
105
106fn set_readonly_property(
107    file: &File,
108    value: &str,
109    force: bool,
110    path: &Path,
111) -> Result<()> {
112    let new_readonly = match value {
113        "true" => true,
114        "false" => false,
115        _ => bail!("invalid value for property: {value}"),
116    };
117
118    let current_flags =
119        subvolume_flags_get(file.as_fd()).with_context(|| {
120            format!("failed to get flags for '{}'", path.display())
121        })?;
122    let is_readonly = current_flags.contains(SubvolumeFlags::RDONLY);
123
124    // No change if already in desired state
125    if is_readonly == new_readonly {
126        return Ok(());
127    }
128
129    // If going from ro to rw, check for received_uuid
130    if is_readonly && !new_readonly {
131        let info = btrfs_uapi::subvolume::subvolume_info(file.as_fd())
132            .with_context(|| {
133                format!("failed to get subvolume info for '{}'", path.display())
134            })?;
135
136        if !info.received_uuid.is_nil() && !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    let mut new_flags = current_flags;
145    if new_readonly {
146        new_flags |= SubvolumeFlags::RDONLY;
147    } else {
148        new_flags &= !SubvolumeFlags::RDONLY;
149    }
150
151    subvolume_flags_set(file.as_fd(), new_flags).with_context(|| {
152        format!("failed to set flags for '{}'", path.display())
153    })?;
154
155    // Clear received_uuid after flipping ro→rw with force.  This must
156    // happen after the flag change (the kernel rejects SET_RECEIVED_SUBVOL
157    // on a read-only subvolume). If it fails, warn but don't error —
158    // matching the C reference behaviour.
159    if is_readonly && !new_readonly && force {
160        let info = btrfs_uapi::subvolume::subvolume_info(file.as_fd()).ok();
161        if let Some(info) = info
162            && !info.received_uuid.is_nil()
163        {
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    Ok(())
182}
183
184fn set_compression_property(
185    file: &File,
186    value: &str,
187    path: &Path,
188) -> Result<()> {
189    use nix::libc::fsetxattr;
190    use std::os::unix::io::AsRawFd;
191
192    let fd = file.as_raw_fd();
193    let xattr_name = "btrfs.compression\0";
194
195    // SAFETY: fsetxattr is safe to call with a valid fd and valid string pointers
196    let result = unsafe {
197        fsetxattr(
198            fd,
199            xattr_name.as_ptr().cast(),
200            value.as_ptr().cast(),
201            value.len(),
202            0,
203        )
204    };
205
206    if result < 0 {
207        return Err(anyhow::anyhow!(
208            "failed to set compression for '{}': {}",
209            path.display(),
210            nix::errno::Errno::last()
211        ));
212    }
213
214    Ok(())
215}