Merge branch 'main' into feat/add-cross-toml

This commit is contained in:
simonsan 2024-11-11 01:51:45 +01:00 committed by GitHub
commit 31005d4339
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 547 additions and 9 deletions

View File

@ -112,7 +112,6 @@ jobs:
sign-release: true
hash-release: true
use-project-version: true
extra-cargo-build-args: --features release
publish-nightly:
if: ${{ github.repository_owner == 'rustic-rs' && github.ref == 'refs/heads/main' }}

View File

@ -91,7 +91,6 @@ jobs:
architecture: armv7
binary-postfix: ""
use-cross: true
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@ -117,7 +116,6 @@ jobs:
hash-release: true
use-project-version: true
use-tag-version: true # IMPORTANT: this is being used to make sure the tag that is built is in the archive filename, so automation can download the correct version
extra-cargo-build-args: --features release
create-release:
name: Creating release with artifacts

74
Cargo.lock generated
View File

@ -109,7 +109,7 @@ dependencies = [
"getrandom",
"once_cell",
"version_check",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@ -1591,6 +1591,33 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fuse_mt"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e098b8dc4cd32e9ba31d9c8cdfef11271d8191233c64c2a671432ff19d354948"
dependencies = [
"fuser",
"libc",
"log",
"threadpool",
]
[[package]]
name = "fuser"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b"
dependencies = [
"libc",
"log",
"memchr",
"page_size",
"pkg-config",
"smallvec",
"zerocopy 0.6.6",
]
[[package]]
name = "futures"
version = "0.3.31"
@ -2966,6 +2993,16 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "page_size"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "pariter"
version = "0.5.1"
@ -3193,7 +3230,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@ -3850,6 +3887,7 @@ dependencies = [
"dircmp",
"directories",
"displaydoc",
"fuse_mt",
"gethostname",
"globset",
"human-panic",
@ -4775,6 +4813,15 @@ dependencies = [
"once_cell",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "time"
version = "0.3.36"
@ -5698,6 +5745,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
dependencies = [
"byteorder",
"zerocopy-derive 0.6.6",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@ -5705,7 +5762,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy-derive"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]]

View File

@ -29,6 +29,7 @@ jemallocator = ["dep:jemallocator-global"]
self-update = ["dep:self_update", "dep:semver"]
tui = ["dep:ratatui", "dep:crossterm", "dep:tui-textarea"]
webdav = ["dep:dav-server", "dep:warp", "dep:tokio", "rustic_core/webdav"]
mount = ["dep:fuse_mt"]
[[bin]]
name = "rustic"
@ -96,6 +97,7 @@ dateparser = "0.2.1"
derive_more = { version = "1", features = ["debug"] }
dialoguer = "0.11.0"
directories = "5"
fuse_mt = { version = "0.6", optional = true }
gethostname = "0.5"
globset = "0.4.15"
human-panic = "2"
@ -123,6 +125,7 @@ toml = "0.8"
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.159"
# cargo-binstall support
# https://github.com/cargo-bins/cargo-binstall/blob/HEAD/SUPPORT.md
[package.metadata.binstall]

40
build-dependencies.just Normal file
View File

@ -0,0 +1,40 @@
### DEFAULT ###
# Install dependencies for the default feature on x86_64-unknown-linux-musl
install-default-x86_64-unknown-linux-musl:
sudo apt-get update
sudo apt-get install -y musl-tools
### MOUNT ###
# Install dependencies for the mount feature on x86_64-unknown-linux-gnu
install-mount-x86_64-unknown-linux-gnu:
sudo apt-get update
sudo apt-get install -y fuse3 libfuse3-dev pkg-config
# Install dependencies for the mount feature on aarch64-unknown-linux-gnu
install-mount-aarch64-unknown-linux-gnu:
sudo apt-get update
sudo apt-get install -y fuse3 libfuse3-dev pkg-config
# Install dependencies for the mount feature on i686-unknown-linux-gnu
install-mount-i686-unknown-linux-gnu:
sudo apt-get update
sudo apt-get install -y fuse3 libfuse3-dev pkg-config
# Install dependencies for the mount feature on x86_64-unknown-linux-musl
install-mount-x86_64-unknown-linux-musl:
sudo apt-get update
sudo apt-get install -y fuse3 libfuse3-dev pkg-config
# Install dependencies for the mount feature on x86_64-apple-darwin
install-mount-x86_64-apple-darwin:
brew install macfuse
# Install dependencies for the mount feature on aarch64-apple-darwin
install-mount-aarch64-apple-darwin:
brew install macfuse
# Install dependencies for the mount feature on x86_64-pc-windows-msvc
install-mount-x86_64-pc-windows-msvc:
winget install winfsp

View File

@ -205,3 +205,11 @@ time-template = "%Y-%m-%d_%H-%M-%S" # only relevant if no snapshot-path is given
symlinks = false
file-access = "read" # Default: "forbidden" for hot/cold repos, else "read"
snapshot-path = "latest:/dir" # Default: not set - if not set, generate a virtual tree with all snapshots using path-template
[mount]
path-template = "[{hostname}]/[{label}]/{time}" # The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. [default: "[{hostname}]/[{label}]/{time}"]. Only relevant if no snapshot-path is given.
time-template = "%Y-%m-%d_%H-%M-%S" # only relevant if no snapshot-path is given
no-allow-other = true
file-access = "read" # Default: "forbidden" for hot/cold repos, else "read"
mountpoint = "~/mnt"
snapshot-path = "latest:/dir" # Default: not set - if not set, generate a virtual tree with all snapshots using path-template

View File

@ -101,6 +101,7 @@ allow = [
"Apache-2.0 WITH LLVM-exception",
"ISC",
"Unicode-DFS-2016",
"BSD-2-Clause",
"BSD-3-Clause",
"MPL-2.0",
"OpenSSL",

10
platform-settings.toml Normal file
View File

@ -0,0 +1,10 @@
[platforms.defaults]
release-features = [
"release",
]
# Check if 'build-dependencies.just' needs to be updated
[platforms.x86_64-unknown-linux-gnu]
additional-features = [
"mount",
]

View File

@ -16,6 +16,8 @@ pub(crate) mod key;
pub(crate) mod list;
pub(crate) mod ls;
pub(crate) mod merge;
#[cfg(feature = "mount")]
pub(crate) mod mount;
pub(crate) mod prune;
pub(crate) mod repair;
pub(crate) mod repoinfo;
@ -34,6 +36,8 @@ use std::fs::File;
use std::path::PathBuf;
use std::str::FromStr;
#[cfg(feature = "mount")]
use crate::commands::mount::MountCmd;
#[cfg(feature = "webdav")]
use crate::commands::webdav::WebDavCmd;
use crate::{
@ -111,6 +115,10 @@ enum RusticCmd {
/// List repository files by file type
List(Box<ListCmd>),
#[cfg(feature = "mount")]
/// Mount a repository as read-only filesystem
Mount(Box<MountCmd>),
/// List file contents of a snapshot
Ls(Box<LsCmd>),
@ -278,6 +286,8 @@ impl Configurable<RusticConfig> for EntryPoint {
RusticCmd::Copy(cmd) => cmd.override_config(config),
#[cfg(feature = "webdav")]
RusticCmd::Webdav(cmd) => cmd.override_config(config),
#[cfg(feature = "mount")]
RusticCmd::Mount(cmd) => cmd.override_config(config),
// subcommands that don't need special overrides use a catch all
_ => Ok(config),

148
src/commands/mount.rs Normal file
View File

@ -0,0 +1,148 @@
//! `mount` subcommand
// ignore markdown clippy lints as we use doc-comments to generate clap help texts
#![allow(clippy::doc_markdown)]
mod fusefs;
use fusefs::FuseFS;
use std::{ffi::OsStr, path::PathBuf};
use crate::{repository::CliIndexedRepo, status_err, Application, RusticConfig, RUSTIC_APP};
use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown};
use anyhow::{anyhow, Result};
use conflate::Merge;
use fuse_mt::{mount, FuseMT};
use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
use serde::{Deserialize, Serialize};
#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct MountCmd {
/// The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. [default: "[{hostname}]/[{label}]/{time}"]
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
path_template: Option<String>,
/// The time template to use to display times in the path template. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for format options. [default: "%Y-%m-%d_%H-%M-%S"]
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
time_template: Option<String>,
/// Don't allow other users to access the mount point
#[clap(long)]
#[merge(strategy=conflate::bool::overwrite_false)]
no_allow_other: bool,
/// How to handle access to files. [default: "forbidden" for hot/cold repositories, else "read"]
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
file_access: Option<String>,
/// The mount point to use
#[clap(value_name = "PATH")]
#[merge(strategy=conflate::option::overwrite_none)]
mountpoint: Option<PathBuf>,
/// Specify directly which snapshot/path to mount
#[clap(value_name = "SNAPSHOT[:PATH]")]
#[merge(strategy=conflate::option::overwrite_none)]
snapshot_path: Option<String>,
}
impl Override<RusticConfig> for MountCmd {
// Process the given command line options, overriding settings from
// a configuration file using explicit flags taken from command-line
// arguments.
fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
let mut self_config = self.clone();
// merge "mount" section from config file, if given
self_config.merge(config.mount);
config.mount = self_config;
Ok(config)
}
}
impl Runnable for MountCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_indexed(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl MountCmd {
fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> {
let config = RUSTIC_APP.config();
let mountpoint = config
.mount
.mountpoint
.as_ref()
.ok_or_else(|| anyhow!("please specify a mountpoint"))?;
let path_template = config
.mount
.path_template
.clone()
.unwrap_or_else(|| "[{hostname}]/[{label}]/{time}".to_string());
let time_template = config
.mount
.time_template
.clone()
.unwrap_or_else(|| "%Y-%m-%d_%H-%M-%S".to_string());
let sn_filter = |sn: &_| config.snapshot_filter.matches(sn);
let vfs = if let Some(snap_path) = &config.mount.snapshot_path {
let node = repo.node_from_snapshot_path(snap_path, sn_filter)?;
Vfs::from_dir_node(&node)
} else {
let snapshots = repo.get_matching_snapshots(sn_filter)?;
Vfs::from_snapshots(
snapshots,
&path_template,
&time_template,
Latest::AsLink,
IdenticalSnapshot::AsLink,
)?
};
let name_opt = format!("fsname=rusticfs:{}", repo.config().id);
let mut options = vec![
OsStr::new("-o"),
OsStr::new(&name_opt),
OsStr::new("-o"),
OsStr::new("kernel_cache"),
];
if !config.mount.no_allow_other {
options.extend_from_slice(&[
OsStr::new("-o"),
OsStr::new("allow_other"),
OsStr::new("-o"),
OsStr::new("default_permissions"),
]);
}
let file_access = config.mount.file_access.as_ref().map_or_else(
|| {
if repo.config().is_hot == Some(true) {
Ok(FilePolicy::Forbidden)
} else {
Ok(FilePolicy::Read)
}
},
|s| s.parse(),
)?;
let fs = FuseMT::new(FuseFS::new(repo, vfs, file_access), 1);
mount(fs, mountpoint, &options)?;
Ok(())
}
}

View File

@ -0,0 +1,238 @@
#[cfg(not(windows))]
use std::os::unix::prelude::OsStrExt;
use std::{
collections::BTreeMap,
ffi::{CString, OsStr},
path::Path,
sync::RwLock,
time::{Duration, SystemTime},
};
use rustic_core::{
repofile::{Node, NodeType},
vfs::{FilePolicy, OpenFile, Vfs},
IndexedFull, Repository,
};
use fuse_mt::{
CallbackResult, DirectoryEntry, FileAttr, FileType, FilesystemMT, RequestInfo, ResultData,
ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice, ResultXattr, Xattr,
};
use itertools::Itertools;
pub struct FuseFS<P, S> {
repo: Repository<P, S>,
vfs: Vfs,
open_files: RwLock<BTreeMap<u64, OpenFile>>,
now: SystemTime,
file_policy: FilePolicy,
}
impl<P, S: IndexedFull> FuseFS<P, S> {
pub(crate) fn new(repo: Repository<P, S>, vfs: Vfs, file_policy: FilePolicy) -> Self {
let open_files = RwLock::new(BTreeMap::new());
Self {
repo,
vfs,
open_files,
now: SystemTime::now(),
file_policy,
}
}
fn node_from_path(&self, path: &Path) -> Result<Node, i32> {
self.vfs
.node_from_path(&self.repo, path)
.map_err(|_| libc::ENOENT)
}
fn dir_entries_from_path(&self, path: &Path) -> Result<Vec<Node>, i32> {
self.vfs
.dir_entries_from_path(&self.repo, path)
.map_err(|_| libc::ENOENT)
}
}
fn node_to_filetype(node: &Node) -> FileType {
match node.node_type {
NodeType::File => FileType::RegularFile,
NodeType::Dir => FileType::Directory,
NodeType::Symlink { .. } => FileType::Symlink,
NodeType::Chardev { .. } => FileType::CharDevice,
NodeType::Dev { .. } => FileType::BlockDevice,
NodeType::Fifo => FileType::NamedPipe,
NodeType::Socket => FileType::Socket,
}
}
fn node_type_to_rdev(tpe: &NodeType) -> u32 {
u32::try_from(match tpe {
NodeType::Dev { device } | NodeType::Chardev { device } => *device,
_ => 0,
})
.unwrap()
}
fn node_to_linktarget(node: &Node) -> Option<&OsStr> {
if node.is_symlink() {
Some(node.node_type.to_link().as_os_str())
} else {
None
}
}
fn node_to_file_attr(node: &Node, now: SystemTime) -> FileAttr {
FileAttr {
// Size in bytes
size: node.meta.size,
// Size in blocks
blocks: 0,
// Time of last access
atime: node.meta.atime.map(SystemTime::from).unwrap_or(now),
// Time of last modification
mtime: node.meta.mtime.map(SystemTime::from).unwrap_or(now),
// Time of last metadata change
ctime: node.meta.ctime.map(SystemTime::from).unwrap_or(now),
// Time of creation (macOS only)
crtime: now,
// Kind of file (directory, file, pipe, etc.)
kind: node_to_filetype(node),
// Permissions
perm: node.meta.mode.unwrap_or(0o755) as u16,
// Number of hard links
nlink: node.meta.links.try_into().unwrap_or(1),
// User ID
uid: node.meta.uid.unwrap_or(0),
// Group ID
gid: node.meta.gid.unwrap_or(0),
// Device ID (if special file)
rdev: node_type_to_rdev(&node.node_type),
// Flags (macOS only; see chflags(2))
flags: 0,
}
}
impl<P, S: IndexedFull> FilesystemMT for FuseFS<P, S> {
fn getattr(&self, _req: RequestInfo, path: &Path, _fh: Option<u64>) -> ResultEntry {
let node = self.node_from_path(path)?;
Ok((Duration::from_secs(1), node_to_file_attr(&node, self.now)))
}
#[cfg(not(windows))]
fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData {
let target = node_to_linktarget(&self.node_from_path(path)?)
.ok_or(libc::ENOSYS)?
.as_bytes()
.to_vec();
Ok(target)
}
fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
if matches!(self.file_policy, FilePolicy::Forbidden) {
return Err(libc::ENOTSUP);
}
let node = self.node_from_path(path)?;
let open = self.repo.open_file(&node).map_err(|_| libc::ENOSYS)?;
let fh = {
let mut open_files = self.open_files.write().unwrap();
let fh = open_files.last_key_value().map_or(0, |(fh, _)| *fh + 1);
_ = open_files.insert(fh, open);
fh
};
Ok((fh, 0))
}
fn release(
&self,
_req: RequestInfo,
_path: &Path,
fh: u64,
_flags: u32,
_lock_owner: u64,
_flush: bool,
) -> ResultEmpty {
_ = self.open_files.write().unwrap().remove(&fh);
Ok(())
}
fn read(
&self,
_req: RequestInfo,
_path: &Path,
fh: u64,
offset: u64,
size: u32,
callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult,
) -> CallbackResult {
if let Some(open_file) = self.open_files.read().unwrap().get(&fh) {
if let Ok(data) =
self.repo
.read_file_at(open_file, offset.try_into().unwrap(), size as usize)
{
return callback(Ok(&data));
}
}
callback(Err(libc::ENOSYS))
}
fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen {
Ok((0, 0))
}
fn readdir(&self, _req: RequestInfo, path: &Path, _fh: u64) -> ResultReaddir {
let nodes = self.dir_entries_from_path(path)?;
let result = nodes
.into_iter()
.map(|node| DirectoryEntry {
name: node.name(),
kind: node_to_filetype(&node),
})
.collect();
Ok(result)
}
fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty {
Ok(())
}
fn listxattr(&self, _req: RequestInfo, path: &Path, size: u32) -> ResultXattr {
let node = self.node_from_path(path)?;
let xattrs = node
.meta
.extended_attributes
.into_iter()
// convert into null-terminated [u8]
.map(|a| CString::new(a.name).unwrap().into_bytes_with_nul())
.concat();
if size == 0 {
Ok(Xattr::Size(u32::try_from(xattrs.len()).unwrap()))
} else {
Ok(Xattr::Data(xattrs))
}
}
fn getxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, size: u32) -> ResultXattr {
let node = self.node_from_path(path)?;
match node
.meta
.extended_attributes
.into_iter()
.find(|a| name == OsStr::new(&a.name))
{
None => Err(libc::ENOSYS),
Some(attr) => {
let value = attr.value.unwrap_or_default();
if size == 0 {
Ok(Xattr::Size(u32::try_from(value.len()).unwrap()))
} else {
Ok(Xattr::Data(value))
}
}
}
}
}

View File

@ -3,7 +3,7 @@
// ignore markdown clippy lints as we use doc-comments to generate clap help texts
#![allow(clippy::doc_markdown)]
use std::{net::ToSocketAddrs, str::FromStr};
use std::net::ToSocketAddrs;
use crate::{repository::CliIndexedRepo, status_err, Application, RusticConfig, RUSTIC_APP};
use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown};
@ -124,7 +124,7 @@ impl WebDavCmd {
Ok(FilePolicy::Read)
}
},
|s| FilePolicy::from_str(s),
|s| s.parse(),
)?;
let dav_server = DavHandler::builder()

View File

@ -18,7 +18,11 @@ use directories::ProjectDirs;
use itertools::Itertools;
use log::Level;
use serde::{Deserialize, Serialize};
#[cfg(not(all(feature = "mount", feature = "webdav")))]
use toml::Value;
#[cfg(feature = "mount")]
use crate::commands::mount::MountCmd;
#[cfg(feature = "webdav")]
use crate::commands::webdav::WebDavCmd;
@ -62,10 +66,21 @@ pub struct RusticConfig {
#[clap(skip)]
pub forget: ForgetOptions,
#[cfg(feature = "webdav")]
/// mount options
#[clap(skip)]
#[cfg(feature = "mount")]
pub mount: MountCmd,
#[cfg(not(feature = "mount"))]
#[merge(skip)]
pub mount: Option<Value>,
/// webdav options
#[clap(skip)]
#[cfg(feature = "webdav")]
pub webdav: WebDavCmd,
#[cfg(not(feature = "webdav"))]
#[merge(skip)]
pub webdav: Option<Value>,
}
impl RusticConfig {