Skip to main content

btrfs_cli/subvolume/
snapshot.rs

1use crate::{Format, Runnable, util::parse_qgroupid};
2use anyhow::{Context, Result};
3use btrfs_uapi::subvolume::snapshot_create;
4use clap::Parser;
5use std::{ffi::CString, fs::File, os::unix::io::AsFd, path::PathBuf};
6
7/// Create a snapshot of a subvolume
8#[derive(Parser, Debug)]
9pub struct SubvolumeSnapshotCommand {
10    /// Make the snapshot read-only
11    #[clap(short, long)]
12    pub readonly: bool,
13
14    /// Add the newly created snapshot to a qgroup (can be given multiple times)
15    #[clap(short = 'i', value_name = "QGROUPID", action = clap::ArgAction::Append)]
16    pub qgroups: Vec<String>,
17
18    /// Path to the source subvolume
19    pub source: PathBuf,
20
21    /// Destination: either an existing directory (snapshot will be named after the source) or a full path for the new snapshot
22    pub dest: PathBuf,
23}
24
25impl Runnable for SubvolumeSnapshotCommand {
26    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
27        let qgroup_ids: Vec<u64> = self
28            .qgroups
29            .iter()
30            .map(|s| parse_qgroupid(s))
31            .collect::<Result<_>>()?;
32
33        let (dest_parent, name_os) =
34            if self.dest.is_dir() {
35                let name_os = self.source.file_name().ok_or_else(|| {
36                    anyhow::anyhow!("source has no file name")
37                })?;
38                (self.dest.as_path(), name_os)
39            } else {
40                let dest_parent = self.dest.parent().ok_or_else(|| {
41                    anyhow::anyhow!("destination has no parent")
42                })?;
43                let name_os = self.dest.file_name().ok_or_else(|| {
44                    anyhow::anyhow!("destination has no name")
45                })?;
46                (dest_parent, name_os)
47            };
48
49        let name_str = name_os.to_str().ok_or_else(|| {
50            anyhow::anyhow!("snapshot name is not valid UTF-8")
51        })?;
52
53        let cname = CString::new(name_str).with_context(|| {
54            format!("snapshot name contains a null byte: '{}'", name_str)
55        })?;
56
57        let source_file = File::open(&self.source).with_context(|| {
58            format!("failed to open source '{}'", self.source.display())
59        })?;
60
61        let parent_file = File::open(dest_parent).with_context(|| {
62            format!(
63                "failed to open destination parent '{}'",
64                dest_parent.display()
65            )
66        })?;
67
68        snapshot_create(
69            parent_file.as_fd(),
70            source_file.as_fd(),
71            &cname,
72            self.readonly,
73            &qgroup_ids,
74        )
75        .with_context(|| {
76            format!(
77                "failed to create snapshot of '{}' in '{}/{}'",
78                self.source.display(),
79                dest_parent.display(),
80                name_str,
81            )
82        })?;
83
84        if self.readonly {
85            println!(
86                "Create readonly snapshot of '{}' in '{}/{}'",
87                self.source.display(),
88                dest_parent.display(),
89                name_str,
90            );
91        } else {
92            println!(
93                "Create snapshot of '{}' in '{}/{}'",
94                self.source.display(),
95                dest_parent.display(),
96                name_str,
97            );
98        }
99
100        Ok(())
101    }
102}