mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
Merge pull request #55 from rustic-rs/special-files+metadata
Special files+metadata
This commit is contained in:
commit
0c2b85da33
35
Cargo.lock
generated
35
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<Path>, dest: impl AsRef<Path>) {
|
||||
pub fn set_times(&self, item: impl AsRef<Path>, 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<Path>, meta: &Metadata) {
|
||||
let mode = *meta.mode();
|
||||
if mode == 0 {
|
||||
return;
|
||||
}
|
||||
pub fn set_user_group(&self, item: impl AsRef<Path>, 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<Path>, 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<Path>, 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<Path>, size: u64) {
|
||||
@ -212,6 +253,38 @@ impl LocalBackend {
|
||||
f.set_len(size).unwrap();
|
||||
}
|
||||
|
||||
pub fn create_special(&self, item: impl AsRef<Path>, 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<Path>, offset: u64, data: &[u8]) {
|
||||
let filename = self.path.join(item);
|
||||
let file = fs::OpenOptions::new()
|
||||
|
||||
@ -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<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mtime: Option<DateTime<Local>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub atime: Option<DateTime<Local>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ctime: Option<DateTime<Local>>,
|
||||
#[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<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gid: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<String>,
|
||||
#[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
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user