diff --git a/Cargo.lock b/Cargo.lock index 54741c7..b5b3a47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,6 +471,18 @@ dependencies = [ "instant", ] +[[package]] +name = "filetime" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.13", + "windows-sys", +] + [[package]] name = "fnv" version = "1.0.7" @@ -916,6 +928,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -934,6 +955,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nix" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1333,6 +1366,7 @@ dependencies = [ "derive-getters", "derive_more", "dirs 4.0.0", + "filetime", "futures", "gethostname", "hex", @@ -1341,6 +1375,7 @@ dependencies = [ "indicatif", "itertools", "lazy_static", + "nix", "path-absolutize", "prettytable-rs", "rand", diff --git a/Cargo.toml b/Cargo.toml index e905393..cc30e6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,8 @@ zstd = "0.11" # local backend walkdir = "2" ignore = "0.4" +nix = "0.24" +filetime = "0.2" # rest backend reqwest = {version = "0.11", default-features = false, features = ["json", "rustls-tls", "stream"] } # cache diff --git a/src/backend/ignore.rs b/src/backend/ignore.rs index ef9b66a..65f372e 100644 --- a/src/backend/ignore.rs +++ b/src/backend/ignore.rs @@ -1,5 +1,5 @@ use std::fs::{read_link, File}; -use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; use anyhow::Result; @@ -183,20 +183,30 @@ fn map_entry(entry: DirEntry, with_atime: bool, cache: &UsersCache) -> Result<(P mtime, atime, ctime, - mode, - uid, - gid, + mode: Some(mode), + uid: Some(uid), + gid: Some(gid), user, group, inode, device_id, links, }; + let filetype = m.file_type(); + let node = if m.is_dir() { Node::new_dir(name, meta) } else if m.is_symlink() { let target = read_link(entry.path())?; Node::new_symlink(name, target, meta) + } else if filetype.is_block_device() { + Node::new_dev(name, meta, m.rdev()) + } else if filetype.is_char_device() { + Node::new_chardev(name, meta, m.rdev()) + } else if filetype.is_fifo() { + Node::new_fifo(name, meta) + } else if filetype.is_socket() { + Node::new_socket(name, meta) } else { Node::new_file(name, meta) }; @@ -207,15 +217,15 @@ const MODE_PERM: u32 = 0o777; // permission bits // consts from https://pkg.go.dev/io/fs#ModeType const GO_MODE_DIR: u32 = 0b10000000000000000000000000000000; -const GO_MODE_SYMLINK: u32 = 0b00000100000000000000000000000000; -const GO_MODE_DEVICE: u32 = 0b00000010000000000000000000000000; -const GO_MODE_FIFO: u32 = 0b00000001000000000000000000000000; -const GO_MODE_SOCKET: u32 = 0b00000000100000000000000000000000; -const GO_MODE_SETUID: u32 = 0b00000000010000000000000000000000; -const GO_MODE_SETGID: u32 = 0b00000000001000000000000000000000; -const GO_MODE_CHARDEV: u32 = 0b00000000000100000000000000000000; -const GO_MODE_STICKY: u32 = 0b00000000000010000000000000000000; -const GO_MODE_IRREG: u32 = 0b00000000000001000000000000000000; +const GO_MODE_SYMLINK: u32 = 0b00001000000000000000000000000000; +const GO_MODE_DEVICE: u32 = 0b00000100000000000000000000000000; +const GO_MODE_FIFO: u32 = 0b00000010000000000000000000000000; +const GO_MODE_SOCKET: u32 = 0b00000001000000000000000000000000; +const GO_MODE_SETUID: u32 = 0b00000000100000000000000000000000; +const GO_MODE_SETGID: u32 = 0b00000000010000000000000000000000; +const GO_MODE_CHARDEV: u32 = 0b00000000001000000000000000000000; +const GO_MODE_STICKY: u32 = 0b00000000000100000000000000000000; +const GO_MODE_IRREG: u32 = 0b00000000000010000000000000000000; // consts from man page inode(7) const S_IFFORMAT: u32 = 0o170000; // File mask @@ -242,7 +252,7 @@ fn map_mode_to_go(mode: u32) -> u32 { S_IFLNK => go_mode |= GO_MODE_SYMLINK, S_IFBLK => go_mode |= GO_MODE_DEVICE, S_IFDIR => go_mode |= GO_MODE_DIR, - S_IFCHR => go_mode |= GO_MODE_CHARDEV, + S_IFCHR => go_mode |= GO_MODE_CHARDEV & GO_MODE_DEVICE, // no idea why go sets both for char devices... S_IFIFO => go_mode |= GO_MODE_FIFO, // note that POSIX specifies regular files, whereas golang specifies irregular files S_IFREG => {} @@ -261,3 +271,38 @@ fn map_mode_to_go(mode: u32) -> u32 { go_mode } + +/// map gloangs mode definition (https://pkg.go.dev/io/fs#ModeType) to t_mode from POSIX (inode(7)) +/// This is the inverse function to map_mode_to_go() +pub fn map_mode_from_go(go_mode: u32) -> u32 { + let mut mode = go_mode & MODE_PERM; + + if go_mode & GO_MODE_SOCKET > 0 { + mode |= S_IFSOCK + } else if go_mode & GO_MODE_SYMLINK > 0 { + mode |= S_IFLNK + } else if go_mode & GO_MODE_DEVICE > 0 && go_mode & GO_MODE_CHARDEV == 0 { + mode |= S_IFBLK; + } else if go_mode & GO_MODE_DIR > 0 { + mode |= S_IFDIR; + } else if go_mode & (GO_MODE_CHARDEV | GO_MODE_DEVICE) > 0 { + mode |= S_IFCHR; + } else if go_mode & GO_MODE_FIFO > 0 { + mode |= S_IFIFO; + } else if go_mode & GO_MODE_IRREG > 0 { + } else { + mode |= S_IFREG; + } + + if go_mode & GO_MODE_SETUID > 0 { + mode |= S_ISUID; + } + if go_mode & GO_MODE_SETGID > 0 { + mode |= S_ISGID; + } + if go_mode & GO_MODE_STICKY > 0 { + mode |= S_ISVTX; + } + + mode +} diff --git a/src/backend/local.rs b/src/backend/local.rs index 56954e6..61068bd 100644 --- a/src/backend/local.rs +++ b/src/backend/local.rs @@ -1,15 +1,19 @@ use std::fs::{self, File}; use std::io::{copy, Read, Seek, SeekFrom, Write}; -use std::os::unix::fs::FileExt; -use std::os::unix::fs::PermissionsExt; +use std::os::unix::fs::{symlink, FileExt, PermissionsExt}; use std::path::{Path, PathBuf}; use anyhow::Result; use async_trait::async_trait; +use filetime::{set_file_atime, set_file_mtime, FileTime}; +use nix::sys::stat::{mknod, Mode, SFlag}; +use nix::unistd::chown; +use nix::unistd::{Gid, Group, Uid, User}; use vlog::*; use walkdir::WalkDir; -use super::{node::Metadata, FileType, Id, ReadBackend, WriteBackend, ALL_FILE_TYPES}; +use super::node::{Metadata, Node, NodeType}; +use super::{map_mode_from_go, FileType, Id, ReadBackend, WriteBackend, ALL_FILE_TYPES}; #[derive(Clone)] pub struct LocalBackend { @@ -190,20 +194,57 @@ impl LocalBackend { fs::create_dir(&dirname).unwrap(); } - pub fn create_symlink(&self, item: impl AsRef, dest: impl AsRef) { + pub fn set_times(&self, item: impl AsRef, meta: &Metadata) -> Result<()> { let filename = self.path.join(item); - std::os::unix::fs::symlink(dest, filename).unwrap(); + if let Some(mtime) = meta.mtime.map(|t| FileTime::from_system_time(t.into())) { + set_file_mtime(&filename, mtime)?; + } + if let Some(atime) = meta.atime.map(|t| FileTime::from_system_time(t.into())) { + set_file_atime(&filename, atime)?; + } + Ok(()) } - // TODO: uid/gid and times - pub fn set_metadata(&self, item: impl AsRef, meta: &Metadata) { - let mode = *meta.mode(); - if mode == 0 { - return; - } + pub fn set_user_group(&self, item: impl AsRef, meta: &Metadata) -> Result<()> { let filename = self.path.join(item); - std::fs::set_permissions(&filename, fs::Permissions::from_mode(mode)) - .unwrap_or_else(|_| panic!("error chmod {:?}", filename)); + + let user = meta + .user + .as_ref() + .and_then(|name| User::from_name(name).unwrap()); + + // use uid from user if valid, else from saved uid (if saved) + let uid = user.map(|u| u.uid).or_else(|| meta.uid.map(Uid::from_raw)); + + let group = meta + .group + .as_ref() + .and_then(|name| Group::from_name(name).unwrap()); + // use gid from group if valid, else from saved gid (if saved) + let gid = group.map(|g| g.gid).or_else(|| meta.gid.map(Gid::from_raw)); + + chown(&filename, uid, gid)?; + Ok(()) + } + + pub fn set_uid_gid(&self, item: impl AsRef, meta: &Metadata) -> Result<()> { + let filename = self.path.join(item); + + let uid = meta.uid.map(Uid::from_raw); + let gid = meta.gid.map(Gid::from_raw); + + chown(&filename, uid, gid)?; + Ok(()) + } + + pub fn set_permission(&self, item: impl AsRef, meta: &Metadata) -> Result<()> { + let filename = self.path.join(item); + + if let Some(mode) = meta.mode() { + let mode = map_mode_from_go(*mode); + std::fs::set_permissions(&filename, fs::Permissions::from_mode(mode))?; + } + Ok(()) } pub fn create_file(&self, item: impl AsRef, size: u64) { @@ -212,6 +253,38 @@ impl LocalBackend { f.set_len(size).unwrap(); } + pub fn create_special(&self, item: impl AsRef, node: &Node) -> Result<()> { + let filename = self.path.join(item); + + match node.node_type() { + NodeType::Symlink { linktarget } => { + symlink(linktarget, filename)?; + } + NodeType::Dev { device } => { + #[cfg(not(target_os = "macos"))] + let device = *device; + #[cfg(target_os = "macos")] + let device = *device as i32; + mknod(&filename, SFlag::S_IFBLK, Mode::empty(), device)?; + } + NodeType::Chardev { device } => { + #[cfg(not(target_os = "macos"))] + let device = *device; + #[cfg(target_os = "macos")] + let device = *device as i32; + mknod(&filename, SFlag::S_IFCHR, Mode::empty(), device)?; + } + NodeType::Fifo => { + mknod(&filename, SFlag::S_IFIFO, Mode::empty(), 0)?; + } + NodeType::Socket => { + mknod(&filename, SFlag::S_IFSOCK, Mode::empty(), 0)?; + } + _ => {} + } + Ok(()) + } + pub fn write_at(&self, item: impl AsRef, offset: u64, data: &[u8]) { let filename = self.path.join(item); let file = fs::OpenOptions::new() diff --git a/src/backend/node.rs b/src/backend/node.rs index fbd56cc..28f1a19 100644 --- a/src/backend/node.rs +++ b/src/backend/node.rs @@ -45,18 +45,18 @@ pub enum NodeType { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Getters)] pub struct Metadata { - #[serde(default, skip_serializing_if = "is_default")] - pub mode: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub mtime: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub atime: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub ctime: Option>, - #[serde(default, skip_serializing_if = "is_default")] - pub uid: u32, - #[serde(default, skip_serializing_if = "is_default")] - pub gid: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub uid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gid: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub user: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -108,6 +108,46 @@ impl Node { } } + pub fn new_dev(name: OsString, meta: Metadata, device: u64) -> Self { + Self { + name: name.to_str().expect("no unicode").to_string(), + node_type: NodeType::Dev { device }, + content: None, + subtree: None, + meta, + } + } + + pub fn new_chardev(name: OsString, meta: Metadata, device: u64) -> Self { + Self { + name: name.to_str().expect("no unicode").to_string(), + node_type: NodeType::Chardev { device }, + content: None, + subtree: None, + meta, + } + } + + pub fn new_fifo(name: OsString, meta: Metadata) -> Self { + Self { + name: name.to_str().expect("no unicode").to_string(), + node_type: NodeType::Fifo, + content: None, + subtree: None, + meta, + } + } + + pub fn new_socket(name: OsString, meta: Metadata) -> Self { + Self { + name: name.to_str().expect("no unicode").to_string(), + node_type: NodeType::Socket, + content: None, + subtree: None, + meta, + } + } + pub fn is_dir(&self) -> bool { self.node_type == NodeType::Dir } diff --git a/src/commands/restore.rs b/src/commands/restore.rs index ea3da4e..c41ce99 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -26,6 +26,10 @@ pub(super) struct Opts { #[clap(long)] delete: bool, + /// use numeric ids instead of user/groug when restoring uid/gui + #[clap(long)] + numeric_id: bool, + /// snapshot to restore id: String, @@ -150,10 +154,20 @@ async fn restore_metadata( let mut node_streamer = NodeStreamer::new(index, tree).await?; while let Some((path, node)) = node_streamer.try_next().await? { if !opts.dry_run { - if let NodeType::Symlink { linktarget } = node.node_type() { - dest.create_symlink(&path, linktarget); + dest.create_special(&path, &node) + .unwrap_or_else(|_| eprintln!("restore {:?}: creating special file failed.", path)); + if opts.numeric_id { + dest.set_uid_gid(&path, node.meta()) + .unwrap_or_else(|_| eprintln!("restore {:?}: setting UID/GID failed.", path)); + } else { + dest.set_user_group(&path, node.meta()).unwrap_or_else(|_| { + eprintln!("restore {:?}: setting User/Group failed.", path) + }); } - dest.set_metadata(&path, node.meta()); + dest.set_permission(&path, node.meta()) + .unwrap_or_else(|_| eprintln!("restore {:?}: chmod failed.", path)); + dest.set_times(&path, node.meta()) + .unwrap_or_else(|_| eprintln!("restore {:?}: setting file times failed.", path)); } }