summaryrefslogtreecommitdiff
path: root/src/cmd/restore.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/restore.rs')
-rw-r--r--src/cmd/restore.rs275
1 files changed, 190 insertions, 85 deletions
diff --git a/src/cmd/restore.rs b/src/cmd/restore.rs
index d783a70..58caf61 100644
--- a/src/cmd/restore.rs
+++ b/src/cmd/restore.rs
@@ -1,101 +1,165 @@
-use crate::client::BackupClient;
-use crate::client::ClientConfig;
+//! The `restore` subcommand.
+
+use crate::backup_reason::Reason;
+use crate::chunk::ClientTrust;
+use crate::client::{BackupClient, ClientError};
+use crate::config::ClientConfig;
+use crate::db::DatabaseError;
+use crate::dbgen::FileId;
use crate::error::ObnamError;
use crate::fsentry::{FilesystemEntry, FilesystemKind};
-use crate::generation::LocalGeneration;
+use crate::generation::{LocalGeneration, LocalGenerationError};
+use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
-use libc::{fchmod, futimens, timespec};
+use libc::{chmod, mkfifo, timespec, utimensat, AT_FDCWD, AT_SYMLINK_NOFOLLOW};
use log::{debug, error, info};
-use std::fs::File;
+use std::ffi::CString;
use std::io::prelude::*;
use std::io::Error;
+use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::symlink;
-use std::os::unix::io::AsRawFd;
+use std::os::unix::net::UnixListener;
+use std::path::StripPrefixError;
use std::path::{Path, PathBuf};
-use structopt::StructOpt;
use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
+
+/// Restore a backup.
+#[derive(Debug, Parser)]
+pub struct Restore {
+ /// Reference to generation to restore.
+ gen_id: String,
+
+ /// Path to directory where restored files are written.
+ to: PathBuf,
+}
+
+impl Restore {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
-pub fn restore(config: &ClientConfig, gen_ref: &str, to: &Path) -> anyhow::Result<()> {
- // Create a named temporary file. We don't meed the open file
- // handle, so we discard that.
- let dbname = {
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
let temp = NamedTempFile::new()?;
- let (_, dbname) = temp.keep()?;
- dbname
- };
- let client = BackupClient::new(&config.server_url)?;
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
- let genlist = client.list_generations()?;
- let gen_id: String = match genlist.resolve(gen_ref) {
- None => return Err(ObnamError::UnknownGeneration(gen_ref.to_string()).into()),
- Some(id) => id,
- };
- info!("generation id is {}", gen_id);
+ let genlist = client.list_generations(&trust);
+ let gen_id = genlist.resolve(&self.gen_id)?;
+ info!("generation id is {}", gen_id.as_chunk_id());
- let gen = client.fetch_generation(&gen_id, &dbname)?;
- info!("restoring {} files", gen.file_count()?);
- let progress = create_progress_bar(gen.file_count()?, true);
- for file in gen.files()? {
- restore_generation(&client, &gen, file.fileno(), file.entry(), &to, &progress)?;
- }
- for file in gen.files()? {
- if file.entry().is_dir() {
- restore_directory_metadata(file.entry(), &to)?;
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ info!("restoring {} files", gen.file_count()?);
+ let progress = create_progress_bar(gen.file_count()?, true);
+ for file in gen.files()?.iter()? {
+ let (fileno, entry, reason, _) = file?;
+ match reason {
+ Reason::FileError => (),
+ _ => restore_generation(&client, &gen, fileno, &entry, &self.to, &progress).await?,
+ }
+ }
+ for file in gen.files()?.iter()? {
+ let (_, entry, _, _) = file?;
+ if entry.is_dir() {
+ restore_directory_metadata(&entry, &self.to)?;
+ }
}
+ progress.finish();
+
+ Ok(())
}
- progress.finish();
+}
- // Delete the temporary file.
- std::fs::remove_file(&dbname)?;
+/// Possible errors from restoring.
+#[derive(Debug, thiserror::Error)]
+pub enum RestoreError {
+ /// An error using a Database.
+ #[error(transparent)]
+ Database(#[from] DatabaseError),
- Ok(())
-}
+ /// Failed to create a name pipe.
+ #[error("Could not create named pipe (FIFO) {0}")]
+ NamedPipeCreationError(PathBuf),
-#[derive(Debug, StructOpt)]
-#[structopt(name = "obnam-backup", about = "Simplistic backup client")]
-struct Opt {
- #[structopt(parse(from_os_str))]
- config: PathBuf,
+ /// Error from HTTP client.
+ #[error(transparent)]
+ ClientError(#[from] ClientError),
- #[structopt()]
- gen_id: String,
+ /// Error from local generation.
+ #[error(transparent)]
+ LocalGenerationError(#[from] LocalGenerationError),
- #[structopt(parse(from_os_str))]
- dbname: PathBuf,
+ /// Error removing a prefix.
+ #[error(transparent)]
+ StripPrefixError(#[from] StripPrefixError),
- #[structopt(parse(from_os_str))]
- to: PathBuf,
+ /// Error creating a directory.
+ #[error("failed to create directory {0}: {1}")]
+ CreateDirs(PathBuf, std::io::Error),
+
+ /// Error creating a file.
+ #[error("failed to create file {0}: {1}")]
+ CreateFile(PathBuf, std::io::Error),
+
+ /// Error writing a file.
+ #[error("failed to write file {0}: {1}")]
+ WriteFile(PathBuf, std::io::Error),
+
+ /// Error creating a symbolic link.
+ #[error("failed to create symbolic link {0}: {1}")]
+ Symlink(PathBuf, std::io::Error),
+
+ /// Error creating a UNIX domain socket.
+ #[error("failed to create UNIX domain socket {0}: {1}")]
+ UnixBind(PathBuf, std::io::Error),
+
+ /// Error setting permissions.
+ #[error("failed to set permissions for {0}: {1}")]
+ Chmod(PathBuf, std::io::Error),
+
+ /// Error settting timestamp.
+ #[error("failed to set timestamp for {0}: {1}")]
+ SetTimestamp(PathBuf, std::io::Error),
}
-fn restore_generation(
+async fn restore_generation(
client: &BackupClient,
gen: &LocalGeneration,
- fileid: i64,
+ fileid: FileId,
entry: &FilesystemEntry,
to: &Path,
progress: &ProgressBar,
-) -> anyhow::Result<()> {
+) -> Result<(), RestoreError> {
info!("restoring {:?}", entry);
- progress.set_message(&format!("{}", entry.pathbuf().display()));
+ progress.set_message(format!("{}", entry.pathbuf().display()));
progress.inc(1);
let to = restored_path(entry, to)?;
match entry.kind() {
- FilesystemKind::Regular => restore_regular(client, &gen, &to, fileid, &entry)?,
+ FilesystemKind::Regular => restore_regular(client, gen, &to, fileid, entry).await?,
FilesystemKind::Directory => restore_directory(&to)?,
- FilesystemKind::Symlink => restore_symlink(&to, &entry)?,
+ FilesystemKind::Symlink => restore_symlink(&to, entry)?,
+ FilesystemKind::Socket => restore_socket(&to, entry)?,
+ FilesystemKind::Fifo => restore_fifo(&to, entry)?,
}
Ok(())
}
-fn restore_directory(path: &Path) -> anyhow::Result<()> {
+fn restore_directory(path: &Path) -> Result<(), RestoreError> {
debug!("restoring directory {}", path.display());
- std::fs::create_dir_all(path)?;
+ std::fs::create_dir_all(path)
+ .map_err(|err| RestoreError::CreateDirs(path.to_path_buf(), err))?;
Ok(())
}
-fn restore_directory_metadata(entry: &FilesystemEntry, to: &Path) -> anyhow::Result<()> {
+fn restore_directory_metadata(entry: &FilesystemEntry, to: &Path) -> Result<(), RestoreError> {
let to = restored_path(entry, to)?;
match entry.kind() {
FilesystemKind::Directory => restore_metadata(&to, entry)?,
@@ -107,7 +171,7 @@ fn restore_directory_metadata(entry: &FilesystemEntry, to: &Path) -> anyhow::Res
Ok(())
}
-fn restored_path(entry: &FilesystemEntry, to: &Path) -> anyhow::Result<PathBuf> {
+fn restored_path(entry: &FilesystemEntry, to: &Path) -> Result<PathBuf, RestoreError> {
let path = &entry.pathbuf();
let path = if path.is_absolute() {
path.strip_prefix("/")?
@@ -117,22 +181,26 @@ fn restored_path(entry: &FilesystemEntry, to: &Path) -> anyhow::Result<PathBuf>
Ok(to.join(path))
}
-fn restore_regular(
+async fn restore_regular(
client: &BackupClient,
gen: &LocalGeneration,
path: &Path,
- fileid: i64,
+ fileid: FileId,
entry: &FilesystemEntry,
-) -> anyhow::Result<()> {
+) -> Result<(), RestoreError> {
debug!("restoring regular {}", path.display());
let parent = path.parent().unwrap();
debug!(" mkdir {}", parent.display());
- std::fs::create_dir_all(parent)?;
+ std::fs::create_dir_all(parent)
+ .map_err(|err| RestoreError::CreateDirs(parent.to_path_buf(), err))?;
{
- let mut file = std::fs::File::create(path)?;
- for chunkid in gen.chunkids(fileid)? {
- let chunk = client.fetch_chunk(&chunkid)?;
- file.write_all(chunk.data())?;
+ let mut file = std::fs::File::create(path)
+ .map_err(|err| RestoreError::CreateFile(path.to_path_buf(), err))?;
+ for chunkid in gen.chunkids(fileid)?.iter()? {
+ let chunkid = chunkid?;
+ let chunk = client.fetch_chunk(&chunkid).await?;
+ file.write_all(chunk.data())
+ .map_err(|err| RestoreError::WriteFile(path.to_path_buf(), err))?;
}
restore_metadata(path, entry)?;
}
@@ -140,24 +208,44 @@ fn restore_regular(
Ok(())
}
-fn restore_symlink(path: &Path, entry: &FilesystemEntry) -> anyhow::Result<()> {
+fn restore_symlink(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
debug!("restoring symlink {}", path.display());
let parent = path.parent().unwrap();
debug!(" mkdir {}", parent.display());
if !parent.exists() {
- std::fs::create_dir_all(parent)?;
- {
- symlink(path, entry.symlink_target().unwrap())?;
+ std::fs::create_dir_all(parent)
+ .map_err(|err| RestoreError::CreateDirs(parent.to_path_buf(), err))?;
+ }
+ symlink(entry.symlink_target().unwrap(), path)
+ .map_err(|err| RestoreError::Symlink(path.to_path_buf(), err))?;
+ restore_metadata(path, entry)?;
+ debug!("restored symlink {}", path.display());
+ Ok(())
+}
+
+fn restore_socket(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
+ debug!("creating Unix domain socket {:?}", path);
+ UnixListener::bind(path).map_err(|err| RestoreError::UnixBind(path.to_path_buf(), err))?;
+ restore_metadata(path, entry)?;
+ Ok(())
+}
+
+fn restore_fifo(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
+ debug!("creating fifo {:?}", path);
+ let filename = path_to_cstring(path);
+ match unsafe { mkfifo(filename.as_ptr(), 0) } {
+ -1 => {
+ return Err(RestoreError::NamedPipeCreationError(path.to_path_buf()));
}
+ _ => restore_metadata(path, entry)?,
}
- debug!("restored regular {}", path.display());
Ok(())
}
-fn restore_metadata(path: &Path, entry: &FilesystemEntry) -> anyhow::Result<()> {
+fn restore_metadata(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
debug!("restoring metadata for {}", entry.pathbuf().display());
- let handle = File::open(path)?;
+ debug!("restoring metadata for {:?}", path);
let atime = timespec {
tv_sec: entry.atime(),
@@ -170,41 +258,58 @@ fn restore_metadata(path: &Path, entry: &FilesystemEntry) -> anyhow::Result<()>
let times = [atime, mtime];
let times: *const timespec = &times[0];
+ let pathbuf = path.to_path_buf();
+ let path = path_to_cstring(path);
+
// We have to use unsafe here to be able call the libc functions
// below.
unsafe {
- let fd = handle.as_raw_fd(); // FIXME: needs to NOT follow symlinks
-
- debug!("fchmod");
- if fchmod(fd, entry.mode()) == -1 {
- let error = Error::last_os_error();
- error!("fchmod failed on {}", path.display());
- return Err(error.into());
+ if entry.kind() != FilesystemKind::Symlink {
+ debug!("chmod {:?}", path);
+ if chmod(path.as_ptr(), entry.mode() as libc::mode_t) == -1 {
+ let error = Error::last_os_error();
+ error!("chmod failed on {:?}", path);
+ return Err(RestoreError::Chmod(pathbuf, error));
+ }
+ } else {
+ debug!(
+ "skipping chmod of a symlink because it'll attempt to change the pointed-at file"
+ );
}
- debug!("futimens");
- if futimens(fd, times) == -1 {
+ debug!("utimens {:?}", path);
+ if utimensat(AT_FDCWD, path.as_ptr(), times, AT_SYMLINK_NOFOLLOW) == -1 {
let error = Error::last_os_error();
- error!("futimens failed on {}", path.display());
- return Err(error.into());
+ error!("utimensat failed on {:?}", path);
+ return Err(RestoreError::SetTimestamp(pathbuf, error));
}
}
Ok(())
}
-fn create_progress_bar(file_count: i64, verbose: bool) -> ProgressBar {
+fn path_to_cstring(path: &Path) -> CString {
+ let path = path.as_os_str();
+ let path = path.as_bytes();
+ CString::new(path).unwrap()
+}
+
+fn create_progress_bar(file_count: FileId, verbose: bool) -> ProgressBar {
let progress = if verbose {
ProgressBar::new(file_count as u64)
} else {
ProgressBar::hidden()
};
- let parts = vec![
+ let parts = [
"{wide_bar}",
"elapsed: {elapsed}",
"files: {pos}/{len}",
"current: {wide_msg}",
"{spinner}",
];
- progress.set_style(ProgressStyle::default_bar().template(&parts.join("\n")));
+ progress.set_style(
+ ProgressStyle::default_bar()
+ .template(&parts.join("\n"))
+ .expect("create indicatif ProgressStyle value"),
+ );
progress
}