diskplan_filesystem/
physical.rs

1use std::{borrow::Cow, fs, io::Write, os::unix::fs::PermissionsExt};
2
3use anyhow::{anyhow, Context, Result};
4use camino::{Utf8Path, Utf8PathBuf};
5use nix::{
6    sys::stat,
7    unistd::{Gid, Uid},
8};
9use users::{Groups, Users, UsersCache};
10
11use super::{
12    attributes::Mode, Attrs, Filesystem, SetAttrs, DEFAULT_DIRECTORY_MODE, DEFAULT_FILE_MODE,
13};
14
15/// Access to a real file system
16#[derive(Default)]
17pub struct DiskFilesystem {
18    users: UsersCache,
19}
20
21impl Filesystem for DiskFilesystem {
22    fn create_directory(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()> {
23        fs::create_dir(path.as_ref())?;
24        self.apply_attrs(path, attrs, DEFAULT_DIRECTORY_MODE)
25    }
26
27    fn create_file(
28        &mut self,
29        path: impl AsRef<Utf8Path>,
30        attrs: SetAttrs,
31        content: String,
32    ) -> Result<()> {
33        let mut file = fs::File::create(path.as_ref())?;
34        file.write_all(content.as_bytes())?;
35        self.apply_attrs(path, attrs, DEFAULT_FILE_MODE)
36    }
37
38    fn create_symlink(
39        &mut self,
40        path: impl AsRef<Utf8Path>,
41        target: impl AsRef<Utf8Path>,
42    ) -> Result<()> {
43        Ok(std::os::unix::fs::symlink(target.as_ref(), path.as_ref())?)
44    }
45
46    fn exists(&self, path: impl AsRef<Utf8Path>) -> bool {
47        fs::metadata(path.as_ref()).is_ok()
48    }
49
50    fn is_directory(&self, path: impl AsRef<Utf8Path>) -> bool {
51        fs::metadata(path.as_ref())
52            .map(|m| m.file_type().is_dir())
53            .unwrap_or(false)
54    }
55
56    fn is_file(&self, path: impl AsRef<Utf8Path>) -> bool {
57        fs::metadata(path.as_ref())
58            .map(|m| m.file_type().is_file())
59            .unwrap_or(false)
60    }
61
62    fn is_link(&self, path: impl AsRef<Utf8Path>) -> bool {
63        fs::symlink_metadata(path.as_ref())
64            .map(|m| m.file_type().is_symlink())
65            .unwrap_or(false)
66    }
67
68    fn list_directory(&self, path: impl AsRef<Utf8Path>) -> Result<Vec<String>> {
69        let mut listing = Vec::new();
70        for entry in fs::read_dir(path.as_ref())? {
71            let entry = entry?;
72            let file_name = entry.file_name();
73            listing.push(file_name.to_string_lossy().into_owned());
74        }
75        Ok(listing)
76    }
77
78    fn read_file(&self, path: impl AsRef<Utf8Path>) -> Result<String> {
79        fs::read_to_string(path.as_ref()).map_err(Into::into)
80    }
81
82    fn read_link(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
83        Ok(fs::read_link(path.as_ref())?.try_into()?)
84    }
85
86    fn attributes(&self, path: impl AsRef<Utf8Path>) -> Result<Attrs> {
87        let stat = stat::stat(path.as_ref().as_std_path())?;
88        let owner = Cow::Owned(
89            self.users
90                .get_user_by_uid(stat.st_uid)
91                .ok_or_else(|| anyhow!("Failed to get user from UID: {}", stat.st_uid))?
92                .name()
93                .to_string_lossy()
94                .into_owned(),
95        );
96        let group = Cow::Owned(
97            self.users
98                .get_group_by_gid(stat.st_gid)
99                .ok_or_else(|| anyhow!("Failed to get group from GID: {}", stat.st_gid))?
100                .name()
101                .to_string_lossy()
102                .into_owned(),
103        );
104        let mode = (stat.st_mode as u16).into();
105        Ok(Attrs { owner, group, mode })
106    }
107
108    fn set_attributes(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()> {
109        let path = path.as_ref();
110        self.apply_attrs(
111            path,
112            attrs,
113            if self.is_directory(path) {
114                DEFAULT_DIRECTORY_MODE
115            } else {
116                DEFAULT_FILE_MODE
117            },
118        )
119    }
120}
121
122impl DiskFilesystem {
123    /// Constructs a new accessor to the on-disk filesystem(s)
124    pub fn new() -> Self {
125        DiskFilesystem {
126            users: UsersCache::new(),
127        }
128    }
129
130    fn apply_attrs(
131        &self,
132        path: impl AsRef<Utf8Path>,
133        attrs: SetAttrs,
134        default_mode: Mode,
135    ) -> Result<()> {
136        let uid = match attrs.owner {
137            Some(owner) => Some(Uid::from_raw(
138                self.users
139                    .get_user_by_name(owner)
140                    .ok_or_else(|| anyhow!("No such user: {}", owner))?
141                    .uid(),
142            )),
143            None => None,
144        };
145        let gid = match attrs.group {
146            Some(group) => Some(Gid::from_raw(
147                self.users
148                    .get_group_by_name(group)
149                    .ok_or_else(|| anyhow!("No such group: {}", group))?
150                    .gid(),
151            )),
152            None => None,
153        };
154        let mode = PermissionsExt::from_mode(attrs.mode.unwrap_or(default_mode).into());
155
156        tracing::trace!("chown {:?} {:?}:{:?}", path.as_ref(), uid, gid);
157        nix::unistd::chown(path.as_ref().as_std_path(), uid, gid)
158            .with_context(|| format!("Changing ownership of {:?}", path.as_ref()))?;
159        fs::set_permissions(path.as_ref(), mode)?;
160        Ok(())
161    }
162}