diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 23e6d77..966b254 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,16 +42,16 @@ jobs: - name: Install i586-unknown-linux-gnu toolchain run: rustup target add i586-unknown-linux-gnu - name: Build for i586-unknown-linux-gnu - run: cargo build --verbose --release --target i586-unknown-linux-gnu + run: cargo build --verbose --release --target i586-unknown-linux-gnu --no-default-features --features minreq - name: Copy i586-unknown-linux-gnu artifact run: cp target/i586-unknown-linux-gnu/release/discord-backup-util target/github-release/discord-backup-util.i586-unknown-linux-gnu - - name: Install i586-unknown-linux-musl toolchain - run: rustup target add i586-unknown-linux-musl - - name: Build for i586-unknown-linux-musl - run: cargo build --verbose --release --target i586-unknown-linux-musl - - name: Copy i586-unknown-linux-musl artifact - run: cp target/i586-unknown-linux-musl/release/discord-backup-util target/github-release/discord-backup-util.i586-unknown-linux-musl + - name: Install i686-unknown-linux-gnu toolchain + run: rustup target add i686-unknown-linux-gnu + - name: Build for i686-unknown-linux-gnu + run: cargo build --verbose --release --target i686-unknown-linux-gnu + - name: Copy i686-unknown-linux-gnu artifact + run: cp target/i686-unknown-linux-gnu/release/discord-backup-util target/github-release/discord-backup-util.i686-unknown-linux-gnu - uses: colathro/crate-version@1.0.0 id: crate-version diff --git a/Cargo.lock b/Cargo.lock index 35acb0c..80809de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.10.4" @@ -150,8 +156,9 @@ dependencies = [ [[package]] name = "discord-backup-util" -version = "0.1.1" +version = "0.2.0" dependencies = [ + "minreq", "rand", "serde", "serde_json", @@ -187,6 +194,21 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -311,12 +333,77 @@ dependencies = [ "adler2", ] +[[package]] +name = "minreq" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" +dependencies = [ + "log", + "openssl", + "openssl-probe", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.3+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "pbkdf2" version = "0.12.2" diff --git a/Cargo.toml b/Cargo.toml index 0f6c176..cb0a398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "discord-backup-util" -version = "0.1.1" +version = "0.2.0" authors = ["buj"] license = "AGPL3-or-later" description = "A tiny tool to backup stuff to Discord" @@ -11,9 +11,15 @@ keywords = ["cli", "discord"] categories = ["command-line-utilities"] edition = "2021" +[features] +default = ["ureq"] +minreq = ["dep:minreq"] +ureq = ["dep:ureq"] + [dependencies] +minreq = { version = "2.12.0", features = ["https-bundled-probe"], optional = true } rand = "0.8.5" serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" -ureq = "2.10.1" +ureq = { version = "2.10.1", optional = true } zip = { version = "2.2.0", features = ["aes", "aes-crypto", "deflate", "deflate-zlib", "deflate64"], default-features = false } diff --git a/README.md b/README.md index e10aa72..bf6d1cd 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,12 @@ A tool that backs up whatever you need to Discord. - Setup a cron job/systemd service to start `discord-backup-util` on boot. - Password-protect the artifacts (they are being uploaded to Discord of all places after all). - Rethink your life choices of why are you backing up your infrastructure to Discord. + +## Building for 32-bit platforms + +We support building `discord-backup-util` down to i586, although build might fail due +to some C packages failing to compile. + +If build fails due to dependencies, add `--no-default-features --features minreq` to command line +(This may take longer to compile as for `minreq` we use bundled OpenSSL instead of RusTLS) (Not all +targets can be fixed this way). diff --git a/src/hook.rs b/src/hook.rs index 9ae3414..3e10016 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -3,6 +3,8 @@ use std::{num::NonZeroU64, ops::Add, time::Duration}; use rand::Rng; use serde::Deserialize; +use crate::log::Logger; + #[derive(Default)] pub struct MessageBuilder(Message); impl MessageBuilder { @@ -10,11 +12,6 @@ impl MessageBuilder { self.0.content.replace(content.into()); self } - pub fn content_maybe(mut self, content: Option>) -> Self { - self.0.content = content.map(|x| x.into()); - self - } - pub fn file(mut self, name: impl Into, file: Vec) -> Self { self.0.files.push((name.into(), file)); self @@ -28,7 +25,7 @@ pub struct Message { pub files: Vec<(String, Vec)>, } impl Message { - pub fn edit(&mut self, hook: &Webhook, text: impl Into) { + pub fn edit(&mut self, hook: &Webhook, text: impl Into, logger: &mut L) { let text: String = text.into(); let Some(id) = self.id else { @@ -38,24 +35,43 @@ impl Message { let body = format!("{{\"content\":{text:?}}}"); self.content.replace(text); + #[allow(clippy::blocks_in_conditions)] loop { - if ureq::patch(&format!("{}/messages/{id}", hook.0)) - .set("Content-Type", "application/json") - .set("Content-Length", &body.len().to_string()) - .send_string(&body) - .is_ok() - { + if { + #[cfg(feature = "minreq")] + { + let body = body.clone(); + minreq::patch(format!("{}/messages/{id}", hook.0)) + .with_header("Content-Type", "application/json") + .with_header("Content-Length", body.len().to_string()) + .with_body(body) + .send() + .is_ok() + } + #[cfg(feature = "ureq")] + { + ureq::patch(&format!("{}/messages/{id}", hook.0)) + .set("Content-Type", "application/json") + .set("Content-Length", &body.len().to_string()) + .send_string(&body) + .is_ok() + } + } { break; } + + logger.info("Failed to edit message, retrying in 10 seconds.."); + std::thread::sleep(Duration::from_secs(10)); } } - pub fn reply( + pub fn reply( &self, hook: &Webhook, message: impl Fn(MessageBuilder) -> MessageBuilder, + logger: &mut L, ) -> Message { - hook.send(message) + hook.send(message, logger) } } @@ -74,7 +90,11 @@ impl Webhook { /// Send a message. /// /// Will try indefinitely until success. - pub fn send(&self, message: impl Fn(MessageBuilder) -> MessageBuilder) -> Message { + pub fn send( + &self, + message: impl Fn(MessageBuilder) -> MessageBuilder, + logger: &mut L, + ) -> Message { let mut message: Message = message(Default::default()).0; let mut bodies: Vec> = vec![]; @@ -127,17 +147,43 @@ impl Webhook { body[ptr..ptr + header.len()].copy_from_slice(&header); } + #[allow(clippy::blocks_in_conditions)] loop { - match ureq::post(&format!("{}?wait=true", self.0)) - .set( - "Content-Type", - &format!("multipart/form-data; boundary={boundary}"), - ) - .set("Content-Length", &body.len().to_string()) - .send_bytes(&body) - { + match { + #[cfg(feature = "minreq")] + { + let body = body.clone(); + minreq::post(format!("{}?wait=true", self.0)) + .with_header( + "Content-Type", + format!("multipart/form-data; boundary={boundary}"), + ) + .with_header("Content-Length", body.len().to_string()) + .with_body(body) + .send() + } + #[cfg(feature = "ureq")] + { + ureq::post(&format!("{}?wait=true", self.0)) + .set( + "Content-Type", + &format!("multipart/form-data; boundary={boundary}"), + ) + .set("Content-Length", &body.len().to_string()) + .send_bytes(&body) + } + } { Ok(x) => { - let parsed: ApiMessage = match serde_json::from_reader(x.into_reader()) { + let parsed: ApiMessage = match serde_json::from_reader({ + #[cfg(feature = "ureq")] + { + x.into_reader() + } + #[cfg(feature = "minreq")] + { + std::io::Cursor::new(x.as_bytes()) + } + }) { Ok(x) => x, Err(why) => { println!("Failed to parse message, retrying in 5 minutes...\n\n{why}"); @@ -151,7 +197,9 @@ impl Webhook { break message; } Err(why) => { - println!("Error sending request: {why}, retrying in 1 minute..."); + logger.error(&format!( + "Error sending request: {why}, retrying in 1 minute..." + )); std::thread::sleep(Duration::from_secs(60)); } } diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..0c44605 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,66 @@ +#![allow(dead_code)] + +use std::ops::DerefMut; + +pub trait Logger { + fn info(&mut self, value: &str); + fn warn(&mut self, value: &str); + fn error(&mut self, value: &str); +} +impl Logger for Box { + fn info(&mut self, value: &str) { + self.deref_mut().info(value) + } + fn warn(&mut self, value: &str) { + self.deref_mut().warn(value) + } + fn error(&mut self, value: &str) { + self.deref_mut().error(value) + } +} +impl Logger for &mut [T] { + fn info(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.info(value)); + } + fn warn(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.warn(value)); + } + fn error(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.error(value)); + } +} +impl Logger for Vec { + fn info(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.info(value)); + } + fn warn(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.warn(value)); + } + fn error(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.error(value)); + } +} +impl Logger for Box<[T]> { + fn info(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.info(value)); + } + fn warn(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.warn(value)); + } + fn error(&mut self, value: &str) { + self.iter_mut().for_each(|x| x.error(value)); + } +} + +pub struct ColorlessPrintlnLogger; +impl Logger for ColorlessPrintlnLogger { + fn info(&mut self, value: &str) { + println!("{value}"); + } + fn warn(&mut self, value: &str) { + println!("{value}"); + } + fn error(&mut self, value: &str) { + println!("{value}"); + } +} diff --git a/src/main.rs b/src/main.rs index 526103b..769cbba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,17 @@ -use std::{ - fs::{self, File}, - io::{Read, Write}, - ops::{Deref, DerefMut}, - path::PathBuf, - process::{Command, Stdio}, -}; +use std::ops::{Deref, DerefMut}; use config::parse_args; -use temp::temp_path; -use zip::{write::SimpleFileOptions, ZipWriter}; +use log::ColorlessPrintlnLogger; +use upload::upload; -pub mod config; -pub mod hook; -pub mod temp; +#[cfg(not(any(feature = "ureq", feature = "minreq")))] +compile_error!("Either 'ureq' or 'minreq' feature must be enabled"); + +mod config; +mod hook; +mod log; +mod temp; +mod upload; struct Defer G>(T, F); impl G> Defer { @@ -51,6 +50,8 @@ impl, G, F: Fn(&mut T) -> G> AsMut for Defer { fn main() { let config = Box::leak(Box::new(parse_args())); + let mut logger = ColorlessPrintlnLogger; + let mut first = true; loop { @@ -60,223 +61,6 @@ fn main() { std::thread::sleep(config.delay); } - println!("Trying to initiate a backup..."); - - let mut head = config - .webhook - .send(|x| x.content("Starting backup process...")); - - let dir = Defer::new(temp_path(), |x| fs::remove_dir_all(x)); - if let Err(why) = fs::create_dir(&*dir) { - println!("Failed to create dir: {why}"); - head.edit(&config.webhook, "Setup failed"); - continue; - } - let script = Defer::new(temp_path(), |x| fs::remove_file(x)); - if let Err(why) = fs::write(&*script, &config.script) { - println!("Failed to write script file: {why}"); - head.edit(&config.webhook, "Setup failed"); - continue; - } - - let mut iter = config.shell.iter(); - let mut proc = match Command::new(iter.next().unwrap()) - .args(iter) - .arg(&*script) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .current_dir(&*dir) - .spawn() - { - Ok(x) => x, - Err(why) => { - println!("Failed to spawn child process: {why}"); - head.edit(&config.webhook, "Failed to start backup process"); - continue; - } - }; - - head.edit(&config.webhook, "Backing up data..."); - - match proc.wait() { - Ok(x) => { - if !x.success() { - println!("Backup process failed: exited with non-zero error code"); - head.edit(&config.webhook, "Backup process failed"); - continue; - } - } - Err(why) => { - println!("Backup process failed: {why}"); - head.edit(&config.webhook, "Backup process failed"); - continue; - } - } - - let archive = Defer::new(temp_path(), |x| fs::remove_file(x)); - - let file = match File::create(&*archive) { - Ok(x) => x, - Err(why) => { - println!("Failed to create temporary file: {why}"); - head.edit(&config.webhook, "Failed to start backup process"); - continue; - } - }; - let mut zip = ZipWriter::new(file); - - head.edit(&config.webhook, "Compressing the archive..."); - - fn walk( - path: PathBuf, - name: String, - zip: &mut ZipWriter, - options: SimpleFileOptions, - ) { - for x in match fs::read_dir(path) { - Ok(x) => x, - Err(why) => { - println!("readdir() failed: {why}"); - return; - } - } { - let x = match x { - Ok(x) => x, - Err(why) => { - println!("readdir() failed: {why}"); - return; - } - }; - - let metadata = match x.metadata() { - Ok(x) => x, - Err(why) => { - println!("metadata() failed: {why}"); - return; - } - }; - - if metadata.is_file() { - if let Err(why) = zip.start_file( - format!("{name}/{}", x.file_name().into_string().unwrap()) - .trim_start_matches('/'), - options.large_file(metadata.len() >= 1024 * 1024 * 1024 * 4), - ) { - println!("Failed to start zip header: {why}"); - return; - } - - let mut buffer = vec![0; 8192]; - let mut file = match File::open(x.path()) { - Ok(x) => x, - Err(why) => { - println!("open() failed: {why}"); - return; - } - }; - - loop { - match file.read(&mut buffer) { - Ok(x) => { - if x == 0 { - break; - } else if let Err(why) = zip.write_all(&buffer[0..x]) { - println!("write() failed: {why}"); - return; - } - } - Err(why) => { - println!("read() failed: {why}"); - return; - } - } - } - - println!( - "Added file {}", - format!("{name}/{}", x.file_name().into_string().unwrap()) - .trim_start_matches('/') - ); - } else { - walk( - x.path(), - format!("{name}/{}", x.file_name().into_string().unwrap()) - .trim_start_matches('/') - .to_string(), - zip, - options, - ); - } - } - } - walk(dir.clone(), String::new(), &mut zip, { - let options = SimpleFileOptions::default() - .compression_level(Some(config.compression_level)) - .compression_method(zip::CompressionMethod::Deflated); - - if let Some(x) = &config.password { - options.with_aes_encryption(zip::AesMode::Aes256, x) - } else { - options - } - }); - - drop(script); - - let mut file = match File::open(&*archive) { - Ok(x) => x, - Err(why) => { - println!("Failed to open temporary file: {why}"); - head.edit(&config.webhook, "Failed to start backup process"); - continue; - } - }; - - if let Err(why) = zip.finish() { - println!("Failed to contruct a zip archive: {why}"); - head.edit(&config.webhook, "Failed to finalize a zip archive"); - continue; - } - - const CHUNK_SIZE: usize = 1000 * 1000 * 25; - let mut buffer = vec![0u8; CHUNK_SIZE]; - let mut chunks = 0u32; - - head.edit(&config.webhook, "Publishing artifact..."); - - loop { - chunks += 1; - let mut ptr = 0usize; - - let mut end = false; - - while ptr != CHUNK_SIZE { - match file.read(&mut buffer) { - Ok(len) => { - ptr += len; - if len == 0 { - end = true; - break; - } - } - Err(why) => { - println!("Failed to upload artifact: {why}"); - head.edit(&config.webhook, "Upload failed"); - continue; - } - } - } - - if ptr == CHUNK_SIZE || end { - head.reply(&config.webhook, move |x| { - x.file(format!("chunk_{chunks}.zip"), buffer[0..ptr].to_vec()) - }); - break; - } - } - - head.edit(&config.webhook, format!("Backup completed successfully.\n\nTo assemble the original archive, download all {chunks} chunks and concatenate them into a single file")); - - println!("Backup completed successfully"); + upload(config, &mut logger); } } diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..8891ccd --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,242 @@ +use std::{ + fs::{self, File}, + io::{Read, Write}, + path::PathBuf, + process::{Command, Stdio}, +}; + +use zip::{write::FileOptions, ZipWriter}; + +use crate::{config::Config, log::Logger, temp::temp_path, Defer}; + +pub fn upload<'a, L: Logger>(config: &'a Config, log: &'a mut L) { + log.info("Trying to initiate a backup..."); + + let mut head = config + .webhook + .send(|x| x.content("Starting backup process..."), log); + + let dir = Defer::new(temp_path(), |x| fs::remove_dir_all(x)); + if let Err(why) = fs::create_dir(&*dir) { + log.error(&format!("Failed to create dir: {why}")); + head.edit(&config.webhook, "Setup failed", log); + return; + } + let script = Defer::new(temp_path(), |x| fs::remove_file(x)); + if let Err(why) = fs::write(&*script, &config.script) { + log.error(&format!("Failed to write script file: {why}")); + head.edit(&config.webhook, "Setup failed", log); + return; + } + + let mut iter = config.shell.iter(); + let mut proc = match Command::new(iter.next().unwrap()) + .args(iter) + .arg(&*script) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .current_dir(&*dir) + .spawn() + { + Ok(x) => x, + Err(why) => { + log.error(&format!("Failed to spawn child process: {why}")); + head.edit(&config.webhook, "Failed to start backup process", log); + return; + } + }; + + head.edit(&config.webhook, "Backing up data...", log); + + match proc.wait() { + Ok(x) => { + if !x.success() { + log.error("Backup process failed: exited with non-zero error code"); + head.edit(&config.webhook, "Backup process failed", log); + return; + } + } + Err(why) => { + log.error(&format!("Backup process failed: {why}")); + head.edit(&config.webhook, "Backup process failed", log); + return; + } + } + + let archive = Defer::new(temp_path(), |x| fs::remove_file(x)); + + let file = match File::create(&*archive) { + Ok(x) => x, + Err(why) => { + log.error(&format!("Failed to create temporary file: {why}")); + head.edit(&config.webhook, "Failed to start backup process", log); + return; + } + }; + let mut zip = ZipWriter::new(file); + + log.info("Compressing the archive..."); + head.edit(&config.webhook, "Compressing the archive...", log); + + fn walk( + path: PathBuf, + name: String, + zip: &mut ZipWriter, + options: FileOptions<'_, ()>, + log: &mut L, + ) { + for x in match fs::read_dir(path) { + Ok(x) => x, + Err(why) => { + log.warn(&format!("readdir() failed: {why}")); + return; + } + } { + let x = match x { + Ok(x) => x, + Err(why) => { + log.warn(&format!("readdir() failed: {why}")); + return; + } + }; + + let metadata = match x.metadata() { + Ok(x) => x, + Err(why) => { + log.warn(&format!("metadata() failed: {why}")); + return; + } + }; + + if metadata.is_file() { + if let Err(why) = zip.start_file( + format!("{name}/{}", x.file_name().into_string().unwrap()) + .trim_start_matches('/'), + options.large_file(metadata.len() >= 1024 * 1024 * 1024 * 4), + ) { + log.warn(&format!("Failed to start zip header: {why}")); + return; + } + + let mut buffer = vec![0; 8192]; + let mut file = match File::open(x.path()) { + Ok(x) => x, + Err(why) => { + log.warn(&format!("open() failed: {why}")); + return; + } + }; + + loop { + match file.read(&mut buffer) { + Ok(x) => { + if x == 0 { + break; + } else if let Err(why) = zip.write_all(&buffer[0..x]) { + log.warn(&format!("write() failed: {why}")); + return; + } + } + Err(why) => { + log.warn(&format!("read() failed: {why}")); + return; + } + } + } + + log.info( + format!("Added file {name}/{}", x.file_name().into_string().unwrap()) + .trim_start_matches('/'), + ); + } else { + walk( + x.path(), + format!("{name}/{}", x.file_name().into_string().unwrap()) + .trim_start_matches('/') + .to_string(), + zip, + options, + log, + ); + } + } + } + let password = config.password.as_ref(); + walk( + dir.clone(), + String::new(), + &mut zip, + { + let options = FileOptions::default() + .compression_level(Some(config.compression_level)) + .compression_method(zip::CompressionMethod::Deflated); + + if let Some(x) = password { + options.with_aes_encryption(zip::AesMode::Aes256, x) + } else { + options + } + }, + log, + ); + + drop(script); + + let mut file = match File::open(&*archive) { + Ok(x) => x, + Err(why) => { + log.error(&format!("Failed to open temporary file: {why}")); + head.edit(&config.webhook, "Failed to start backup process", log); + return; + } + }; + + if let Err(why) = zip.finish() { + log.error(&format!("Failed to contruct a zip archive: {why}")); + head.edit(&config.webhook, "Failed to finalize a zip archive", log); + return; + } + + const CHUNK_SIZE: usize = 1000 * 1000 * 25; + let mut buffer = vec![0u8; CHUNK_SIZE]; + let mut chunks = 0u32; + + head.edit(&config.webhook, "Publishing artifact...", log); + + loop { + chunks += 1; + let mut ptr = 0usize; + + let mut end = false; + + while ptr != CHUNK_SIZE { + match file.read(&mut buffer) { + Ok(len) => { + ptr += len; + if len == 0 { + end = true; + break; + } + } + Err(why) => { + log.error(&format!("Failed to upload artifact: {why}")); + head.edit(&config.webhook, "Upload failed", log); + continue; + } + } + } + + if ptr == CHUNK_SIZE || end { + head.reply( + &config.webhook, + move |x| x.file(format!("chunk_{chunks}.zip"), buffer[0..ptr].to_vec()), + log, + ); + break; + } + } + + head.edit(&config.webhook, format!("Backup completed successfully.\n\nTo assemble the original archive, download all {chunks} chunks and concatenate them into a single file"), log); + + println!("Backup completed successfully"); +}