mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
Merge pull request #522 from rustic-rs/xattr
Handle extended file attributes
This commit is contained in:
commit
c7e134102c
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -1829,7 +1829,6 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"backoff",
|
||||
"base64 0.21.0",
|
||||
"binrw",
|
||||
"bytes",
|
||||
"bytesize",
|
||||
@ -1883,6 +1882,7 @@ dependencies = [
|
||||
"toml",
|
||||
"users",
|
||||
"walkdir",
|
||||
"xattr 1.0.0",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
@ -2267,7 +2267,7 @@ checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
"xattr 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2868,6 +2868,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea263437ca03c1522846a4ddafbca2542d0ad5ed9b784909d4b27b76f62bc34a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.4.3"
|
||||
|
||||
@ -42,10 +42,10 @@ scrypt = { version = "0.11", default-features = false }
|
||||
# cdc = "0.1"
|
||||
integer-sqrt = "0.1"
|
||||
# serialization
|
||||
base64 = "0.21"
|
||||
binrw = "0.11"
|
||||
hex = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_with = { version = "2.3", features = ["base64"] }
|
||||
serde_json = "1"
|
||||
serde-aux = "4"
|
||||
# other dependencies
|
||||
@ -76,7 +76,6 @@ directories = "5"
|
||||
nom = "7"
|
||||
toml = "0.7"
|
||||
merge = "0.1"
|
||||
serde_with = "2.3"
|
||||
rpassword = "7"
|
||||
bytesize = "1"
|
||||
indicatif = "0.17"
|
||||
@ -92,6 +91,7 @@ rhai = {version = "1.13", features = ["sync", "serde", "no_optimize", "no_module
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
users = "0.11"
|
||||
xattr = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.17"
|
||||
|
||||
@ -17,8 +17,8 @@ use serde_with::{serde_as, DisplayFromStr};
|
||||
#[cfg(not(windows))]
|
||||
use users::{Groups, Users, UsersCache};
|
||||
|
||||
use super::{node::Metadata, node::NodeType, Node, ReadSource};
|
||||
use super::{ReadSourceEntry, ReadSourceOpen};
|
||||
use super::node::{ExtendedAttribute, Metadata, NodeType};
|
||||
use super::{Node, ReadSource, ReadSourceEntry, ReadSourceOpen};
|
||||
|
||||
pub struct LocalSource {
|
||||
builder: WalkBuilder,
|
||||
@ -259,6 +259,7 @@ fn map_entry(
|
||||
inode,
|
||||
device_id,
|
||||
links,
|
||||
extended_attributes: Vec::new(),
|
||||
};
|
||||
|
||||
let node = if m.is_dir() {
|
||||
@ -322,6 +323,16 @@ fn map_entry(
|
||||
let device_id = if ignore_devid { 0 } else { m.dev() };
|
||||
let links = if m.is_dir() { 0 } else { m.nlink() };
|
||||
|
||||
let path = entry.path();
|
||||
let extended_attributes = xattr::list(path)?
|
||||
.map(|name| {
|
||||
Ok(ExtendedAttribute {
|
||||
name: name.to_string_lossy().to_string(),
|
||||
value: xattr::get(path, name)?.unwrap(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let meta = Metadata {
|
||||
size,
|
||||
mtime,
|
||||
@ -335,6 +346,7 @@ fn map_entry(
|
||||
inode,
|
||||
device_id,
|
||||
links,
|
||||
extended_attributes,
|
||||
};
|
||||
let filetype = m.file_type();
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ use crate::repository::parse_command;
|
||||
use super::mapper::map_mode_from_go;
|
||||
#[cfg(not(windows))]
|
||||
use super::node::NodeType;
|
||||
use super::node::{Metadata, Node};
|
||||
use super::node::{ExtendedAttribute, Metadata, Node};
|
||||
use super::{FileType, Id, ReadBackend, WriteBackend, ALL_FILE_TYPES};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -324,6 +324,35 @@ impl LocalDestination {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn set_extended_attributes(
|
||||
&self,
|
||||
item: impl AsRef<Path>,
|
||||
extended_attributes: &[ExtendedAttribute],
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn set_extended_attributes(
|
||||
&self,
|
||||
item: impl AsRef<Path>,
|
||||
extended_attributes: &[ExtendedAttribute],
|
||||
) -> Result<()> {
|
||||
let filename = self.path(item);
|
||||
|
||||
for name in xattr::list(&filename)? {
|
||||
if let Err(err) = xattr::remove(&filename, &name) {
|
||||
warn!("error removing xattr on {filename:?}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
for ExtendedAttribute { name, value } in extended_attributes {
|
||||
xattr::set(&filename, name, value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// set_length sets the length of the given file. If it doesn't exist, create a new (empty) one with given length
|
||||
pub fn set_length(&self, item: impl AsRef<Path>, size: u64) -> Result<()> {
|
||||
let filename = self.path(item);
|
||||
|
||||
@ -14,6 +14,8 @@ use derive_getters::Getters;
|
||||
use derive_more::{Constructor, IsVariant};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_aux::prelude::*;
|
||||
use serde_with::base64::Base64;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::id::Id;
|
||||
|
||||
@ -68,6 +70,16 @@ pub struct Metadata {
|
||||
pub device_id: u64,
|
||||
pub size: u64,
|
||||
pub links: u64,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub extended_attributes: Vec<ExtendedAttribute>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExtendedAttribute {
|
||||
pub name: String,
|
||||
#[serde_as(as = "Base64")]
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
|
||||
|
||||
@ -453,6 +453,8 @@ fn set_metadata(dest: &LocalDestination, path: &PathBuf, node: &Node, opts: &Opt
|
||||
}
|
||||
dest.set_permission(path, node.meta())
|
||||
.unwrap_or_else(|_| warn!("restore {:?}: chmod failed.", path));
|
||||
dest.set_extended_attributes(path, &node.meta.extended_attributes)
|
||||
.unwrap_or_else(|_| warn!("restore {:?}: setting extended attributes failed.", path));
|
||||
dest.set_times(path, node.meta())
|
||||
.unwrap_or_else(|_| warn!("restore {:?}: setting file times failed.", path));
|
||||
}
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use chrono::{DateTime, Local};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use scrypt::Params;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::base64::Base64;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::backend::{FileType, ReadBackend};
|
||||
use crate::crypto::{CryptoKey, Key};
|
||||
use crate::id::Id;
|
||||
|
||||
#[serde_as]
|
||||
#[serde_with::apply(Option => #[serde(default, skip_serializing_if = "Option::is_none")])]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct KeyFile {
|
||||
@ -20,8 +22,10 @@ pub struct KeyFile {
|
||||
n: u32,
|
||||
r: u32,
|
||||
p: u32,
|
||||
data: String,
|
||||
salt: String,
|
||||
#[serde_as(as = "Base64")]
|
||||
data: Vec<u8>,
|
||||
#[serde_as(as = "Base64")]
|
||||
salt: Vec<u8>,
|
||||
}
|
||||
|
||||
impl KeyFile {
|
||||
@ -29,10 +33,10 @@ impl KeyFile {
|
||||
fn kdf_key(&self, passwd: &impl AsRef<[u8]>) -> Result<Key> {
|
||||
let params = Params::new(log_2(self.n), self.r, self.p, Params::RECOMMENDED_LEN)
|
||||
.map_err(|_| anyhow!("invalid scrypt paramters"))?;
|
||||
let salt = STANDARD.decode(&self.salt)?;
|
||||
|
||||
let mut key = [0; 64];
|
||||
scrypt::scrypt(passwd.as_ref(), &salt, ¶ms, &mut key).expect("output length invalid?");
|
||||
scrypt::scrypt(passwd.as_ref(), &self.salt, ¶ms, &mut key)
|
||||
.expect("output length invalid?");
|
||||
|
||||
Ok(Key::from_slice(&key))
|
||||
}
|
||||
@ -41,7 +45,7 @@ impl KeyFile {
|
||||
/// The key usually should be the key generated by [`kdf_key()`](Self::kdf_key)
|
||||
fn key_from_data(&self, key: &Key) -> Result<Key> {
|
||||
let dec_data = key
|
||||
.decrypt_data(&STANDARD.decode(&self.data)?)
|
||||
.decrypt_data(&self.data)
|
||||
.map_err(|_| anyhow!("decryption failed"))?;
|
||||
serde_json::from_slice::<MasterKey>(&dec_data)?.key()
|
||||
}
|
||||
@ -62,7 +66,7 @@ impl KeyFile {
|
||||
) -> Result<Self> {
|
||||
let masterkey = MasterKey::from_key(key);
|
||||
let params = Params::recommended();
|
||||
let mut salt = [0; 64];
|
||||
let mut salt = vec![0; 64];
|
||||
thread_rng().fill_bytes(&mut salt);
|
||||
|
||||
let mut key = [0; 64];
|
||||
@ -79,8 +83,8 @@ impl KeyFile {
|
||||
r: params.r(),
|
||||
p: params.p(),
|
||||
created: with_created.then(Local::now),
|
||||
data: STANDARD.encode(data),
|
||||
salt: STANDARD.encode(salt),
|
||||
data,
|
||||
salt,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -102,36 +106,34 @@ fn log_2(x: u32) -> u8 {
|
||||
num_bits::<u32>() as u8 - x.leading_zeros() as u8 - 1
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Mac {
|
||||
k: String,
|
||||
r: String,
|
||||
#[serde_as(as = "Base64")]
|
||||
k: Vec<u8>,
|
||||
#[serde_as(as = "Base64")]
|
||||
r: Vec<u8>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct MasterKey {
|
||||
mac: Mac,
|
||||
encrypt: String,
|
||||
#[serde_as(as = "Base64")]
|
||||
encrypt: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MasterKey {
|
||||
fn from_key(key: Key) -> Self {
|
||||
let (encrypt, k, r) = key.to_keys();
|
||||
Self {
|
||||
encrypt: STANDARD.encode(encrypt),
|
||||
mac: Mac {
|
||||
k: STANDARD.encode(k),
|
||||
r: STANDARD.encode(r),
|
||||
},
|
||||
encrypt,
|
||||
mac: Mac { k, r },
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&self) -> Result<Key> {
|
||||
Ok(Key::from_keys(
|
||||
&STANDARD.decode(&self.encrypt)?,
|
||||
&STANDARD.decode(&self.mac.k)?,
|
||||
&STANDARD.decode(&self.mac.r)?,
|
||||
))
|
||||
Ok(Key::from_keys(&self.encrypt, &self.mac.k, &self.mac.r))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user