diff options
author | metamuffin <metamuffin@disroot.org> | 2024-06-14 04:48:14 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-06-14 04:48:14 +0200 |
commit | e4499d44e931d2ee7c70e7610bbfca3fefd00fea (patch) | |
tree | 470e44d4ca22de6404aeb4330784dd568751908b | |
parent | e34da6ca957ec813a361cbcaf0dc89e953af6db1 (diff) | |
download | online-offsite-backup-e4499d44e931d2ee7c70e7610bbfca3fefd00fea.tar online-offsite-backup-e4499d44e931d2ee7c70e7610bbfca3fefd00fea.tar.bz2 online-offsite-backup-e4499d44e931d2ee7c70e7610bbfca3fefd00fea.tar.zst |
implement upload and download
-rw-r--r-- | readme.md | 48 | ||||
-rw-r--r-- | src/client.rs | 58 | ||||
-rw-r--r-- | src/main.rs | 76 | ||||
-rw-r--r-- | src/server.rs | 8 |
4 files changed, 171 insertions, 19 deletions
@@ -31,3 +31,51 @@ Backups are stored as-is on the remote server: If your backup requires it, encrypt it! ## Usage + +1. Intall the CLI with cargo: + `cargo install --path /path/to/online-offsite-backup` + +2. Place a configuration file somewhere. + ```toml + [storage] + root = "/home/muffin/temp/backuptest" + size = 1_000_000_000 # maximum size of a single backup version + versions = 3 # number of backups to keep + upload_cooldown = 864000 # cooldown in seconds for backup uploads + download_cooldown = 43200 # cooldown in seconds for backup downloads + upload_speed = 3_000_000 # maximum sustained backup upload speed in bytes per second + download_speed = 3_000_000 # maximum sustained backup download speed in bytes per second + + [server] + address = "127.0.0.1:29285" + + [[peer]] + name = "alice" + address = "aliceserver.net:29285" + shared_secret = "hunter2" + + [[peer]] + name = "bob" + address = "bobserver.net:29285" + shared_secret = "p4ssw0rd" + ``` +3. Start uploading a backup: `backuptool config.toml upload /path/to/backup` +4. List currently stored backups: `backuptool config.toml list` +5. Restore a remote backup by serial: + `backuptool config.toml download 2 /path/to/restore` +6. See further usage `backuptool --help` + +## Protocol + +Connect to the server via TCP and follow this simple text-based protocol. +Whenever there is an error the server replies `error,<message>` and then +disconnects you.First send a line with the shared secret, then send commands. + +- `list`: Client sends `list` on a line. The server replies with multiple lines + in the format `<mtime>:<size>:<serial>` ended with a single empty line. +- `upload`: Client sends `upload,<size>` on a line. If accepted the server + replies `ready`. The client should now send exactly as many bytes as + announced. Once done the server replies `done` on a line. +- `download`: Client sends `download,<serial>` on a line. If accepted the server + replies `ready,<size>` on a line and then continues sending exactly this many + bytes before concluding with `done` on a line. diff --git a/src/client.rs b/src/client.rs index 492008a..0f86712 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, bail, Result}; +use log::debug; use std::{ - io::{BufRead, BufReader, BufWriter, Write}, + io::{copy, BufRead, BufReader, BufWriter, Read, Write}, net::{TcpStream, ToSocketAddrs}, }; @@ -25,8 +26,9 @@ impl Client { let mut line = String::new(); let mut out = Vec::new(); loop { + line.clear(); self.rsock.read_line(&mut line)?; - if line.trim().is_empty() || line.ends_with("\n\n") { + if line.trim().is_empty() { break Ok(out); } check_error(&line)?; @@ -35,6 +37,56 @@ impl Client { out.push((mtime.parse()?, size.parse()?, serial.parse()?)); } } + pub fn upload(&mut self, size: u64, mut reader: impl Read) -> Result<()> { + debug!("announcing upload of {size} bytes"); + writeln!(self.wsock, "upload,{size}")?; + self.wsock.flush()?; + + let mut line = String::new(); + self.rsock.read_line(&mut line)?; + check_error(&line)?; + if line.trim() != "ready" { + bail!("invalid response, not ready"); + } + debug!("server ready"); + + copy(&mut reader, &mut self.wsock)?; + + line.clear(); + self.rsock.read_line(&mut line)?; + check_error(&line)?; + if line.trim() != "done" { + bail!("invalid response, not done"); + } + debug!("server done"); + Ok(()) + } + pub fn download(&mut self, serial: Serial, mut writer: impl Write) -> Result<()> { + debug!("requesting download for serial={serial}"); + writeln!(self.wsock, "download,{serial}")?; + self.wsock.flush()?; + + let mut line = String::new(); + self.rsock.read_line(&mut line)?; + check_error(&line)?; + let Some(size) = line.trim().strip_prefix("ready,") else { + bail!("server not ready") + }; + eprintln!("{size:?}"); + let size = size.parse()?; + debug!("server ready"); + + copy(&mut self.rsock.by_ref().take(size), &mut writer)?; + + line.clear(); + self.rsock.read_line(&mut line)?; + check_error(&line)?; + if line.trim() != "done" { + bail!("invalid response, not done"); + } + debug!("server done"); + Ok(()) + } pub fn quit(mut self) -> Result<()> { writeln!(self.wsock, "quit")?; self.wsock.flush()?; @@ -43,7 +95,7 @@ impl Client { } fn check_error(line: &str) -> Result<()> { - if let Some(message) = line.trim().strip_prefix("error") { + if let Some(message) = line.trim().strip_prefix("error,") { bail!("server error: {message}") } Ok(()) diff --git a/src/main.rs b/src/main.rs index c72aad7..22977ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,18 @@ pub mod client; pub mod server; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use client::Client; +use log::info; use serde::Deserialize; use server::server; -use std::{fs::read_to_string, net::SocketAddr, path::PathBuf}; +use std::{ + fs::{read_to_string, File}, + net::SocketAddr, + os::unix::fs::MetadataExt, + path::PathBuf, +}; pub type Serial = u64; @@ -24,13 +30,18 @@ enum Action { List { peer: Option<String>, }, - Restore { - peer: String, - id: String, - destination: PathBuf, + Download { + path: PathBuf, + /// Which peer to download from, any if not specified + peer: Option<String>, + /// Serial of the backup to download, latest if not specified + serial: Option<Serial>, }, - Backup { + Upload { + /// Path to backup file path: PathBuf, + /// Which peer to upload to, all if not specified + peer: Option<String>, }, } @@ -91,12 +102,51 @@ fn main() -> Result<()> { client.quit()?; } } - Action::Restore { - id, - destination, - peer, - } => todo!(), - Action::Backup { path } => todo!(), + Action::Download { path, serial, peer } => { + let mut peers = config.peer.iter().filter(|p| { + if let Some(pn) = &peer { + pn == &p.name + } else { + true + } + }); + let peer = peers.next().ok_or(anyhow!("no matching peer"))?; + info!("connecting to {:?}", peer.name); + let mut client = Client::new(peer.address, &peer.shared_secret)?; + + let file = File::create_new(&path)?; + let serial = if let Some(serial) = serial { + serial + } else { + client.list()?.last().ok_or(anyhow!("no backups stored"))?.2 + }; + println!("downloading serial={serial} from {}", peer.name); + client.download(serial, file)?; + info!("upload successful"); + client.quit()?; + println!("success") + } + Action::Upload { peer, path } => { + let peers = config.peer.iter().filter(|p| { + if let Some(pn) = &peer { + pn == &p.name + } else { + true + } + }); + for peer in peers { + info!("connecting to {:?}", peer.name); + println!("uploading to {}", peer.name); + let mut client = Client::new(peer.address, &peer.shared_secret)?; + + let file = File::open(&path)?; + let size = file.metadata()?.size(); + client.upload(size, file)?; + info!("upload successful"); + client.quit()?; + } + println!("success") + } } Ok(()) } diff --git a/src/server.rs b/src/server.rs index d24f1b4..93baeb6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,6 @@ use crate::{Config, PeerConfig, Serial}; use anyhow::{anyhow, bail}; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::{ fs::{create_dir_all, read_to_string, remove_file, File}, @@ -78,6 +78,7 @@ fn handle_connection( rsock.by_ref().take(4096).read_line(&mut line)?; let mut toks = line.trim().split(","); let command = toks.next().ok_or(anyhow!("command missing"))?; + debug!("command {command:?} issued"); match command { "quit" => break Ok(()), "list" => { @@ -123,7 +124,7 @@ fn handle_connection( writeln!(wsock, "ready")?; wsock.flush()?; - while peerdir.read_dir()?.fold(0, |a, _| a + 1) > config.storage.versions { + while peerdir.read_dir()?.fold(0, |a, _| a + 1) >= config.storage.versions { let mut dir = peerdir .read_dir()? .map(|e| { @@ -143,7 +144,7 @@ fn handle_connection( remove_file(peerdir.join(rem.to_string()))?; } - info!("upload from {addr} size={size}"); + info!("upload from {addr} size={size} serial={serial}"); let mut upload = rsock.get_ref().take(size); let mut target = BufWriter::new(File::create_new(peerdir.join(serial.to_string()))?); @@ -177,6 +178,7 @@ fn handle_connection( let size = source.metadata()?.size(); let mut source = BufReader::new(source); + info!("download for {addr} size={size} serial={serial}"); writeln!(wsock, "ready,{size}")?; wsock.flush()?; copy(&mut source, wsock)?; // TODO speed limit |