aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-06-14 04:48:14 +0200
committermetamuffin <metamuffin@disroot.org>2024-06-14 04:48:14 +0200
commite4499d44e931d2ee7c70e7610bbfca3fefd00fea (patch)
tree470e44d4ca22de6404aeb4330784dd568751908b
parente34da6ca957ec813a361cbcaf0dc89e953af6db1 (diff)
downloadonline-offsite-backup-e4499d44e931d2ee7c70e7610bbfca3fefd00fea.tar
online-offsite-backup-e4499d44e931d2ee7c70e7610bbfca3fefd00fea.tar.bz2
online-offsite-backup-e4499d44e931d2ee7c70e7610bbfca3fefd00fea.tar.zst
implement upload and download
-rw-r--r--readme.md48
-rw-r--r--src/client.rs58
-rw-r--r--src/main.rs76
-rw-r--r--src/server.rs8
4 files changed, 171 insertions, 19 deletions
diff --git a/readme.md b/readme.md
index b3c1929..7053403 100644
--- a/readme.md
+++ b/readme.md
@@ -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