diff options
Diffstat (limited to 'src/cmd/restore.rs')
-rw-r--r-- | src/cmd/restore.rs | 275 |
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 = ×[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 } |