aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-06-14 03:35:24 +0200
committermetamuffin <metamuffin@disroot.org>2024-06-14 03:35:24 +0200
commit956b172ddbff214d055127fb89905f0057492a26 (patch)
tree675512962d99e59ff4cae5fd6153b8468b22f541
downloadonline-offsite-backup-956b172ddbff214d055127fb89905f0057492a26.tar
online-offsite-backup-956b172ddbff214d055127fb89905f0057492a26.tar.bz2
online-offsite-backup-956b172ddbff214d055127fb89905f0057492a26.tar.zst
server kinda works
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock421
-rw-r--r--Cargo.toml12
-rw-r--r--readme.md33
-rw-r--r--src/main.rs69
-rw-r--r--src/server.rs228
6 files changed, 765 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fabfb87
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/config.toml
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..350b44e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,421 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
+
+[[package]]
+name = "clap"
+version = "4.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
+name = "env_filter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "memchr"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d0d8b92cd8358e8d229c11df9358decae64d137c5be540952c5ca7b25aea768"
+
+[[package]]
+name = "offsite-vault"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "env_logger",
+ "log",
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "winnow"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
+dependencies = [
+ "memchr",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ad34b07
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "offsite-vault"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+toml = "0.8.14"
+serde = { version = "1.0.203", features = ["derive"] }
+log = "0.4.21"
+env_logger = "0.11.3"
+anyhow = "1.0.86"
+clap = { version = "4.5.7", features = ["derive"] }
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..b3c1929
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,33 @@
+# online-offsite-backup (name to change)
+
+This tool provides a way to offer others a service for backing up important
+files automatically.
+
+Problem: Offsite backups could be as easy as a network filesystem - the problem
+is however that these usually implement deleting files aswell. In the case where
+a user's machine is comprimised by an attacker, these files could just be
+deleted in the same way they were created, defeating the advantages of an
+**offsite** backup.
+
+Solution: Implement a network "filesystem" that only supports non-destructive
+operations. Only implementing upload means that backups will accumulate on the
+server. This tool deletes backups after N newer backups (like a ring buffer). To
+prevent quick successive uploads as a means of deleting backups a cooldown for
+backup uploads is implemented.
+
+Intended Usage: This program is developed to be deployed on a number of servers
+that are not under your control like backing up data of your friends. In such a
+scenario, everyone in your friend group would run this software on their server
+and negotiate keys with every other one.
+
+Possible attacks: This software primarily protects against the exact case
+mentioned above especially when been attacked by automated malware on your
+machine. Your remote backups servers may still be vulnerable to social
+engineering and supply-chain attacks.
+
+Security: This software assumes security (and reliability) of the TCP
+connections it makes: You **must** implement protection on this level yourself.
+Backups are stored as-is on the remote server: If your backup requires it,
+encrypt it!
+
+## Usage
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..243c621
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,69 @@
+#![feature(iterator_try_collect)]
+#![feature(never_type)]
+pub mod server;
+
+use clap::{Parser, Subcommand};
+use serde::Deserialize;
+use server::server;
+use std::{fs::read_to_string, net::SocketAddr, path::PathBuf};
+
+pub type Serial = u64;
+
+#[derive(Parser)]
+struct Args {
+ config: PathBuf,
+ #[clap(subcommand)]
+ action: Action,
+}
+
+#[derive(Subcommand)]
+enum Action {
+ Daemon,
+ Restore { id: String, destination: PathBuf },
+ Backup { path: PathBuf },
+}
+
+#[derive(Deserialize)]
+pub struct Config {
+ storage: StorageConfig,
+ server: ServerConfig,
+ peer: Vec<PeerConfig>,
+}
+
+#[derive(Deserialize)]
+pub struct PeerConfig {
+ name: String,
+ address: SocketAddr,
+ shared_secret: String,
+}
+
+#[derive(Deserialize)]
+pub struct ServerConfig {
+ address: String,
+}
+
+#[derive(Deserialize)]
+pub struct StorageConfig {
+ root: PathBuf,
+ size: u64,
+ versions: usize,
+ upload_cooldown: u64,
+ download_cooldown: u64,
+ upload_speed: usize,
+ download_speed: usize,
+}
+
+fn main() -> anyhow::Result<()> {
+ env_logger::init_from_env("LOG");
+ let args = Args::parse();
+
+ let config = read_to_string(&args.config)?;
+ let config = toml::from_str::<Config>(&config)?;
+
+ match args.action {
+ Action::Daemon => server(config.into())?,
+ Action::Restore { id, destination } => todo!(),
+ Action::Backup { path } => todo!(),
+ }
+ Ok(())
+}
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..30ed0c8
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,228 @@
+use crate::{Config, PeerConfig, Serial};
+use anyhow::{anyhow, bail};
+use log::{error, info, warn};
+use serde::{Deserialize, Serialize};
+use std::{
+ fs::{create_dir_all, read_to_string, remove_file, File},
+ io::{copy, BufRead, BufReader, BufWriter, Read, Write},
+ net::{SocketAddr, TcpListener, TcpStream},
+ os::unix::fs::MetadataExt,
+ sync::{Arc, Mutex},
+ thread::spawn,
+ time::SystemTime,
+};
+
+pub fn server(config: Arc<Config>) -> anyhow::Result<!> {
+ let listener = TcpListener::bind(&config.server.address)?;
+ info!("listening on {}", listener.local_addr()?);
+ loop {
+ let Ok((sock, addr)) = listener.accept() else {
+ error!("could not accept connection");
+ continue;
+ };
+ info!("connection from {addr}");
+ let config = config.clone();
+ spawn(
+ move || match handle_connection_wrapper(config, sock, addr) {
+ Ok(()) => (),
+ Err(err) => {
+ warn!("client error {addr}: {err:?}")
+ }
+ },
+ );
+ }
+}
+
+fn handle_connection_wrapper(
+ config: Arc<Config>,
+ sock: TcpStream,
+ addr: SocketAddr,
+) -> anyhow::Result<()> {
+ let mut rsock = BufReader::new(sock.try_clone()?);
+ let mut wsock = BufWriter::new(sock);
+ match handle_connection(config, &mut rsock, &mut wsock, addr) {
+ Ok(()) => Ok(()),
+ Err(e) => {
+ writeln!(wsock, "error,{e}")?;
+ wsock.flush()?;
+ Err(e)
+ }
+ }
+}
+fn handle_connection(
+ config: Arc<Config>,
+ rsock: &mut BufReader<TcpStream>,
+ wsock: &mut BufWriter<TcpStream>,
+ addr: SocketAddr,
+) -> anyhow::Result<()> {
+ let mut line = String::new();
+
+ rsock.by_ref().take(4096).read_line(&mut line)?;
+
+ let provided_secret = line.trim();
+ eprintln!("{:?}", provided_secret);
+ let Some(peer) = config
+ .peer
+ .iter()
+ .find(|p| p.shared_secret == provided_secret)
+ else {
+ bail!("invalid secret");
+ };
+
+ let peerdir = config.storage.root.join(&peer.name);
+ create_dir_all(&peerdir)?;
+
+ loop {
+ line.clear();
+ rsock.by_ref().take(4096).read_line(&mut line)?;
+ let mut toks = line.trim().split(",");
+ let command = toks.next().ok_or(anyhow!("command missing"))?;
+ eprintln!("{command:?}");
+ match command {
+ "quit" => break Ok(()),
+ "list" => {
+ let mut dir = peerdir
+ .read_dir()?
+ .map(|e| {
+ let e = e?;
+ Ok::<_, anyhow::Error>((
+ e.metadata()?.mtime(),
+ e.metadata()?.size(),
+ e.file_name()
+ .to_str()
+ .unwrap()
+ .to_string()
+ .parse::<Serial>()?,
+ ))
+ })
+ .try_collect::<Vec<_>>()?;
+ dir.sort_by_key(|(_, _, a)| *a);
+
+ for (m, s, p) in dir {
+ writeln!(wsock, "{m}:{s}:{p}")?;
+ }
+ writeln!(wsock)?;
+ wsock.flush()?;
+ }
+ "upload" => {
+ let size = toks.next().ok_or(anyhow!("size missing"))?.parse::<u64>()?;
+ if size > config.storage.size {
+ bail!("maximum size exceeded")
+ }
+
+ let serial = transact_user_state(&config, peer, |s| {
+ if s.last_upload.elapsed().unwrap().as_secs() > config.storage.upload_cooldown {
+ s.last_upload = SystemTime::now();
+ s.serial += 1;
+ Some(s.serial)
+ } else {
+ None
+ }
+ })?
+ .ok_or(anyhow!("upload cooldown"))?;
+ writeln!(wsock, "ready")?;
+ wsock.flush()?;
+
+ while peerdir.read_dir()?.fold(0, |a, _| a + 1) > config.storage.versions {
+ let mut dir = peerdir
+ .read_dir()?
+ .map(|e| {
+ let e = e?;
+ Ok::<_, anyhow::Error>(
+ e.file_name()
+ .to_str()
+ .unwrap()
+ .to_string()
+ .parse::<Serial>()?,
+ )
+ })
+ .try_collect::<Vec<_>>()?;
+ dir.sort();
+ let rem = dir[0];
+ info!("removing serial={rem}");
+ remove_file(peerdir.join(rem.to_string()))?;
+ }
+
+ info!("upload from {addr} size={size}");
+ let mut upload = rsock.get_ref().take(size);
+ let mut target =
+ BufWriter::new(File::create_new(peerdir.join(serial.to_string()))?);
+
+ copy(&mut upload, &mut target)?; // TODO speed limit
+
+ info!("done {addr}");
+ writeln!(wsock, "done")?;
+ wsock.flush()?;
+ }
+ "download" => {
+ let serial = toks
+ .next()
+ .ok_or(anyhow!("serial missing"))?
+ .parse::<Serial>()?;
+ let ok = transact_user_state(&config, peer, |s| {
+ if s.last_download.elapsed().unwrap().as_secs()
+ > config.storage.download_cooldown
+ {
+ s.last_download = SystemTime::now();
+ true
+ } else {
+ false
+ }
+ })?;
+ if !ok {
+ bail!("download cooldown")
+ }
+
+ let source = File::open(peerdir.join(serial.to_string()))?;
+ let size = source.metadata()?.size();
+ let mut source = BufReader::new(source);
+
+ writeln!(wsock, "ready,{size}")?;
+ wsock.flush()?;
+ copy(&mut source, wsock)?; // TODO speed limit
+
+ writeln!(wsock, "done")?;
+ wsock.flush()?;
+ }
+ _ => bail!("unknown command"),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct PeerState {
+ last_upload: SystemTime,
+ last_download: SystemTime,
+ serial: Serial,
+}
+
+static USER_DB: Mutex<()> = Mutex::new(());
+fn transact_user_state<T>(
+ config: &Config,
+ peer: &PeerConfig,
+ update: impl FnOnce(&mut PeerState) -> T,
+) -> anyhow::Result<T> {
+ let _g = USER_DB
+ .lock()
+ .map_err(|_| anyhow!("user database locked"))?;
+
+ let conf_path = config.storage.root.join(&peer.name).with_extension("db");
+
+ let mut state = read_to_string(&conf_path)
+ .map(|s| toml::from_str(&s))
+ .unwrap_or_else(|_| {
+ Ok(PeerState {
+ last_download: SystemTime::UNIX_EPOCH,
+ last_upload: SystemTime::UNIX_EPOCH,
+ serial: 0,
+ })
+ })?;
+
+ let res = update(&mut state);
+ let ser_state = toml::to_string_pretty(&state)?;
+
+ File::create(conf_path)?.write_all(ser_state.as_bytes())?;
+
+ drop(_g);
+ Ok(res)
+}