use crate::common;
use crate::error::LibError;
use crate::qgroup::QgroupInherit;
use crate::subvolume::SubvolumeInfo;
use crate::Result;
use std::convert::TryFrom;
use std::ffi::CString;
use std::path::{Path, PathBuf};
use btrfsutil_sys::btrfs_util_create_snapshot;
use btrfsutil_sys::btrfs_util_create_subvolume;
use btrfsutil_sys::btrfs_util_delete_subvolume;
use btrfsutil_sys::btrfs_util_deleted_subvolumes;
use btrfsutil_sys::btrfs_util_get_default_subvolume;
use btrfsutil_sys::btrfs_util_get_subvolume_read_only;
use btrfsutil_sys::btrfs_util_is_subvolume;
use btrfsutil_sys::btrfs_util_set_default_subvolume;
use btrfsutil_sys::btrfs_util_set_subvolume_read_only;
use btrfsutil_sys::btrfs_util_subvolume_id;
use btrfsutil_sys::btrfs_util_subvolume_path;
use btrfsutil_sys::btrfs_util_wait_sync;
use libc::{c_void, free};
bitflags! {
pub struct DeleteFlags: i32 {
const RECURSIVE = btrfsutil_sys::BTRFS_UTIL_DELETE_SUBVOLUME_RECURSIVE as i32;
}
}
bitflags! {
pub struct SnapshotFlags: i32 {
const READ_ONLY = btrfsutil_sys::BTRFS_UTIL_CREATE_SNAPSHOT_READ_ONLY as i32;
const RECURSIVE = btrfsutil_sys::BTRFS_UTIL_CREATE_SNAPSHOT_RECURSIVE as i32;
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Subvolume {
id: u64,
path: PathBuf,
}
impl Subvolume {
pub fn get<'a, P>(path: P) -> Result<Self>
where
P: Into<&'a Path>,
{
Self::get_impl(path.into())
}
fn get_impl(path: &Path) -> Result<Self> {
Self::is_subvolume(path)?;
let path_cstr = common::path_to_cstr(path);
let id: u64 = {
let mut id: u64 = 0;
unsafe_wrapper!({ btrfs_util_subvolume_id(path_cstr.as_ptr(), &mut id) })?;
id
};
Ok(Subvolume::new(id, path.into()))
}
pub fn get_anyway<'a, P>(path: P) -> Result<Self>
where
P: Into<&'a Path>,
{
Self::get_anyway_impl(path.into())
}
fn get_anyway_impl(path: &Path) -> Result<Self> {
if let Ok(subvol) = Self::get_impl(path) {
return Ok(subvol);
}
let path_cstr = common::path_to_cstr(path);
let id: u64 = {
let mut id: u64 = 0;
unsafe_wrapper!({ btrfs_util_subvolume_id(path_cstr.as_ptr(), &mut id) })?;
id
};
let mut path_ret_ptr: *mut std::os::raw::c_char = std::ptr::null_mut();
unsafe_wrapper!({ btrfs_util_subvolume_path(path_cstr.as_ptr(), id, &mut path_ret_ptr) })?;
let path_ret: CString = unsafe { CString::from_raw(path_ret_ptr) };
Ok(Self::new(id, common::cstr_to_path(&path_ret)))
}
pub fn create<'a, P, Q>(path: P, qgroup: Q) -> Result<Self>
where
P: Into<&'a Path>,
Q: Into<Option<QgroupInherit>>,
{
Self::create_impl(path.into(), qgroup.into())
}
fn create_impl(path: &Path, qgroup: Option<QgroupInherit>) -> Result<Self> {
let path_cstr = common::path_to_cstr(path);
let qgroup_ptr = qgroup.map(|v| v.as_ptr()).unwrap_or(std::ptr::null_mut());
let transid: u64 = {
let mut transid: u64 = 0;
unsafe_wrapper!({
btrfs_util_create_subvolume(path_cstr.as_ptr(), 0, &mut transid, qgroup_ptr)
})?;
transid
};
unsafe_wrapper!({ btrfs_util_wait_sync(path_cstr.as_ptr(), transid) })?;
Self::get(path)
}
pub fn delete<D>(self, flags: D) -> Result<()>
where
D: Into<Option<DeleteFlags>>,
{
Self::delete_impl(self, flags.into())
}
fn delete_impl(self, flags: Option<DeleteFlags>) -> Result<()> {
let path_cstr = common::path_to_cstr(&self.path);
let flags_val = flags.map(|v| v.bits()).unwrap_or(0);
unsafe_wrapper!({ btrfs_util_delete_subvolume(path_cstr.as_ptr(), flags_val) })?;
Ok(())
}
pub fn deleted<'a, F>(fs_root: F) -> Result<Vec<Self>>
where
F: Into<&'a Path>,
{
Self::deleted_impl(fs_root.into())
}
fn deleted_impl(fs_root: &Path) -> Result<Vec<Subvolume>> {
let path_cstr = common::path_to_cstr(fs_root);
let mut ids_ptr: *mut u64 = std::ptr::null_mut();
let mut ids_count: usize = 0;
unsafe_wrapper!({
btrfs_util_deleted_subvolumes(path_cstr.as_ptr(), &mut ids_ptr, &mut ids_count)
})?;
if ids_count == 0 {
return Ok(Vec::new());
}
let subvolume_ids: Vec<u64> = unsafe {
let slice = std::slice::from_raw_parts(ids_ptr, ids_count);
let vec = slice.to_vec();
free(ids_ptr as *mut c_void);
vec
};
let subvolumes: Vec<Subvolume> = {
let mut subvolumes: Vec<Subvolume> = Vec::with_capacity(ids_count);
for id in subvolume_ids {
subvolumes.push(Subvolume::try_from(id)?);
}
subvolumes
};
Ok(subvolumes)
}
pub fn get_default<'a, P>(path: P) -> Result<Self>
where
P: Into<&'a Path>,
{
Self::get_default_impl(path.into())
}
fn get_default_impl(path: &Path) -> Result<Self> {
let path_cstr = common::path_to_cstr(path);
let mut id: u64 = 0;
unsafe_wrapper!({ btrfs_util_get_default_subvolume(path_cstr.as_ptr(), &mut id) })?;
Ok(Subvolume::new(id, path.into()))
}
pub fn set_default(&self) -> Result<()> {
let path_cstr = common::path_to_cstr(&self.path);
unsafe_wrapper!({ btrfs_util_set_default_subvolume(path_cstr.as_ptr(), self.id) })?;
Ok(())
}
pub fn is_ro(&self) -> Result<bool> {
let path_cstr = common::path_to_cstr(&self.path);
let ro: bool = {
let mut ro = false;
unsafe_wrapper!({ btrfs_util_get_subvolume_read_only(path_cstr.as_ptr(), &mut ro) })?;
ro
};
Ok(ro)
}
pub fn set_ro(&self, ro: bool) -> Result<()> {
let path_cstr = common::path_to_cstr(&self.path);
unsafe_wrapper!({ btrfs_util_set_subvolume_read_only(path_cstr.as_ptr(), ro) })?;
Ok(())
}
pub fn is_subvolume<'a, P>(path: P) -> Result<()>
where
P: Into<&'a Path>,
{
Self::is_subvolume_impl(path.into())
}
fn is_subvolume_impl(path: &Path) -> Result<()> {
let path_cstr = common::path_to_cstr(path);
unsafe_wrapper!({ btrfs_util_is_subvolume(path_cstr.as_ptr()) })
}
pub fn info(&self) -> Result<SubvolumeInfo> {
SubvolumeInfo::try_from(self)
}
pub fn snapshot<'a, P, F, Q>(&self, path: P, flags: F, qgroup: Q) -> Result<Self>
where
P: Into<&'a Path>,
F: Into<Option<SnapshotFlags>>,
Q: Into<Option<QgroupInherit>>,
{
self.snapshot_impl(path.into(), flags.into(), qgroup.into())
}
fn snapshot_impl(
&self,
path: &Path,
flags: Option<SnapshotFlags>,
qgroup: Option<QgroupInherit>,
) -> Result<Self> {
let path_src_cstr = common::path_to_cstr(&self.path);
let path_dest_cstr = common::path_to_cstr(path);
let flags_val = flags.map(|v| v.bits()).unwrap_or(0);
let qgroup_ptr = qgroup.map(|v| v.as_ptr()).unwrap_or(std::ptr::null_mut());
let transid: u64 = {
let mut transid: u64 = 0;
unsafe_wrapper!({
btrfs_util_create_snapshot(
path_src_cstr.as_ptr(),
path_dest_cstr.as_ptr(),
flags_val,
&mut transid,
qgroup_ptr,
)
})?;
transid
};
unsafe_wrapper!({ btrfs_util_wait_sync(path_dest_cstr.as_ptr(), transid) })?;
Self::get(path)
}
#[inline]
pub fn id(&self) -> u64 {
self.id
}
#[inline]
pub fn path(&self) -> &Path {
&self.path
}
#[inline]
pub(crate) fn new(id: u64, path: PathBuf) -> Self {
Self { id, path }
}
}
impl From<&Subvolume> for u64 {
#[inline]
fn from(subvolume: &Subvolume) -> u64 {
subvolume.id
}
}
impl TryFrom<u64> for Subvolume {
type Error = LibError;
fn try_from(src: u64) -> Result<Subvolume> {
let path_cstr: CString = common::path_to_cstr(
std::env::current_dir()
.expect("Could not get the current working directory")
.as_ref(),
);
let mut path_ret_ptr: *mut std::os::raw::c_char = std::ptr::null_mut();
unsafe_wrapper!({ btrfs_util_subvolume_path(path_cstr.as_ptr(), src, &mut path_ret_ptr) })?;
let path_ret: CString = unsafe { CString::from_raw(path_ret_ptr) };
Ok(Self::new(src, common::cstr_to_path(&path_ret)))
}
}
impl From<&Subvolume> for PathBuf {
#[inline]
fn from(subvolume: &Subvolume) -> Self {
subvolume.path.clone()
}
}
impl<'lifetime> From<&'lifetime Subvolume> for &'lifetime Path {
#[inline]
fn from(subvolume: &'lifetime Subvolume) -> Self {
subvolume.path.as_ref()
}
}
impl TryFrom<&Path> for Subvolume {
type Error = LibError;
#[inline]
fn try_from(src: &Path) -> Result<Subvolume> {
Subvolume::get_impl(src)
}
}
impl TryFrom<PathBuf> for Subvolume {
type Error = LibError;
#[inline]
fn try_from(src: PathBuf) -> Result<Subvolume> {
Subvolume::get_impl(src.as_ref())
}
}
#[cfg(test)]
mod test {
use super::*;
use std::fs::{create_dir_all, OpenOptions};
use std::path::Path;
use nix::mount::{mount, MsFlags};
use crate::testing::{btrfs_create_fs, test_with_spec};
use btrfsutil_sys::BTRFS_FS_TREE_OBJECTID;
fn test_btrfs_subvol(paths: &[&Path]) {
btrfs_create_fs(paths[0]).unwrap();
let mount_pt = Path::new("/tmp/btrfsutil/mnt");
create_dir_all(mount_pt).unwrap();
mount(
Some(paths[0]),
mount_pt,
Some("btrfs"),
MsFlags::empty(),
None as Option<&str>,
)
.unwrap();
let root_subvol = Subvolume::try_from(mount_pt).unwrap();
assert_eq!(root_subvol.id(), BTRFS_FS_TREE_OBJECTID);
let mut new_sv_path = mount_pt.to_owned();
new_sv_path.push("subvol1");
let sv1 = Subvolume::create(&*new_sv_path, None).unwrap();
let sv1_abs_path = sv1.path().to_owned();
assert_eq!(&sv1_abs_path, &new_sv_path, "paths are not equal");
let default_sv = Subvolume::get_default(mount_pt).unwrap();
assert_eq!(
default_sv, root_subvol,
"default subvolume is not the root subvolume"
);
sv1.set_default().unwrap();
let new_default_sv = Subvolume::get_default(mount_pt).unwrap();
assert_eq!(sv1, new_default_sv, "new default subvolume not set");
assert_eq!(
new_default_sv.path().canonicalize().unwrap(),
new_sv_path,
"default subvolume path does not match"
);
root_subvol.set_default().unwrap();
let info = root_subvol.info().unwrap();
assert_eq!(info.id, BTRFS_FS_TREE_OBJECTID);
assert_eq!(info.parent_id, None);
assert_eq!(info.dir_id, None);
assert_eq!(info.parent_uuid, None);
assert_eq!(info.received_uuid, None);
assert_eq!(false, sv1.is_ro().unwrap());
sv1.set_ro(true).unwrap();
let mut file_path = sv1_abs_path.clone();
file_path.push("file.txt");
assert!(OpenOptions::new()
.write(true)
.create_new(true)
.open(&file_path)
.is_err());
sv1.set_ro(false).unwrap();
assert!(OpenOptions::new()
.write(true)
.create_new(true)
.open(&file_path)
.is_ok());
Subvolume::is_subvolume(mount_pt).expect("Valid subvolume failed is_subvolume test");
Subvolume::is_subvolume(&*new_sv_path).expect("Valid subvolume failed is_subvolume test");
Subvolume::is_subvolume(Path::new("/tmp"))
.expect_err("Existing, non-btrfs path incorrectly flagged as subvolume");
Subvolume::is_subvolume(Path::new("/foobar"))
.expect_err("Nonexistent path incorrectly flagged as subvolume");
let mut dir_path = sv1_abs_path.clone();
dir_path.push("dir1");
create_dir_all(&dir_path).unwrap();
Subvolume::is_subvolume(&*dir_path)
.expect_err("Directory within a subvolume incorrectly flagged as subvolume");
let mut snap_path = mount_pt.to_owned();
snap_path.push("snap1");
let snap_sv1 = sv1.snapshot(&*snap_path, None, None).unwrap();
let mut snap_file_path = snap_path;
snap_file_path.push("file.txt");
assert!(OpenOptions::new().read(true).open(&snap_file_path).is_ok());
let snap_id = snap_sv1.info().unwrap().id;
snap_sv1.delete(None).unwrap();
let deleted = Subvolume::deleted(mount_pt).unwrap();
assert_eq!(1, deleted.len());
assert_eq!(snap_id, deleted[0].id());
}
#[test]
#[ignore] fn loop_test_btrfs_subvol() {
test_with_spec(1, test_btrfs_subvol);
}
}