From e40f1bb3d6ff31195880b4efadcd5447c496ff8d Mon Sep 17 00:00:00 2001 From: lucalise Date: Wed, 4 Feb 2026 19:20:51 -0800 Subject: [PATCH] feat!: add copying for cross fs support --- Cargo.lock | 1 + Cargo.toml | 1 + src/archive.rs | 2 +- src/config.rs | 4 +-- src/error.rs | 6 ++++ src/main.rs | 1 - src/reporter.rs | 38 ++++++++++++++++++++++++-- src/storage/fs.rs | 68 ++++++++++++++++++++++++++++++++++++++-------- src/storage/mod.rs | 6 ++-- 9 files changed, 106 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac3da9a..c0dd84d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,6 +401,7 @@ dependencies = [ "dirs", "flate2", "indicatif", + "libc", "serde", "shellexpand", "tar", diff --git a/Cargo.toml b/Cargo.toml index 030bf9e..609eaa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ ctrlc = "3.5.1" dirs = "6.0.0" flate2 = "1.1.8" indicatif = "0.18.3" +libc = "0.2.180" serde = { version = "1.0.228", features = ["derive"] } shellexpand = "3.1.1" tar = "0.4.44" diff --git a/src/archive.rs b/src/archive.rs index 70f0b67..cdae811 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -25,7 +25,7 @@ pub fn create_archive(app_state: Data, reporter: &Reporter) -> Result { - let target = crate::storage::fs::base_path(); + let target = crate::storage::fs::base_path_local(); tempfile::Builder::new() .prefix(".revision_") .suffix(".tar.gz.tmp") diff --git a/src/config.rs b/src/config.rs index dde93bf..336c075 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{ error::{Error, Result}, - storage::fs::base_path, + storage::fs::base_path_local, }; #[derive(Serialize, Deserialize)] @@ -19,7 +19,7 @@ pub struct Sync { impl Config { pub fn new() -> Result { - let path = base_path().join("config.toml"); + let path = base_path_local().join("config.toml"); let bytes = match fs::read(&path) { Ok(b) => b, Err(e) => match e.kind() { diff --git a/src/error.rs b/src/error.rs index 97833e2..aeadbd1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,12 @@ pub enum ErrorKind { #[source] source: std::io::Error, }, + #[error("error copying file to {path}")] + Copy { + path: PathBuf, + #[source] + source: std::io::Error, + }, } pub type Result = core::result::Result; diff --git a/src/main.rs b/src/main.rs index 620c829..086b3dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,6 @@ use tracing_subscriber::{ use crate::{ config::Config, - reporter::Reporter, storage::{Storage, StorageImpl, get_storage_from_env}, }; diff --git a/src/reporter.rs b/src/reporter.rs index 7b7df0a..c2423e4 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -1,10 +1,11 @@ use std::borrow::Cow; -use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; +use indicatif::{HumanBytes, ProgressBar, ProgressDrawTarget, ProgressStyle}; use crate::Data; pub struct Reporter { + app_state: Data, spinner: ProgressBar, } @@ -20,7 +21,7 @@ impl Reporter { ); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); spinner.set_draw_target(ProgressDrawTarget::stderr_with_hz(20)); - Self { spinner } + Self { app_state, spinner } } pub fn start_archive_progress(&self, total: u64) { @@ -52,6 +53,39 @@ impl Reporter { self.spinner.set_style( ProgressStyle::with_template("{msg:>12.green} ✓ [{elapsed_precise}]").unwrap(), ); + self.spinner.set_prefix(""); self.spinner.finish_with_message(msg); } + + pub fn start_bytes_progress( + &self, + total_bytes: u64, + operation: impl Into>, + ) -> ProgressBar { + let bar = self.app_state.multi.add(ProgressBar::new(total_bytes)); + bar.set_style( + ProgressStyle::with_template( + "{msg:10.214/yellow} {spinner} [{elapsed_precise}] [{bar:30.yellow/blue}] {bytes}/{total_bytes}", + ) + .unwrap() + .tick_chars(TICK_CHARS) + .progress_chars("█▉▊▋▌▍▎▏ "), + ); + bar.enable_steady_tick(std::time::Duration::from_millis(100)); + bar.set_draw_target(ProgressDrawTarget::stderr_with_hz(20)); + bar.set_message(operation); + bar + } + + pub fn finish_bytes_progress(bar: &ProgressBar, msg: impl Into>) { + let total = bar.length().unwrap_or(0); + bar.set_style( + ProgressStyle::with_template(&format!( + "{{msg:>12.green}} ✓ [{{elapsed_precise}}] {}", + HumanBytes(total) + )) + .unwrap(), + ); + bar.finish_with_message(msg); + } } diff --git a/src/storage/fs.rs b/src/storage/fs.rs index 8ad2af5..74a4fc5 100644 --- a/src/storage/fs.rs +++ b/src/storage/fs.rs @@ -1,6 +1,9 @@ -use std::io::{self}; +use std::io::{self, BufReader, BufWriter, Read, Write}; +use std::path::Path; use std::{fs, path::PathBuf}; +use indicatif::ProgressBar; + use crate::Data; use crate::archive::create_archive; use crate::error::{Error, Result}; @@ -11,9 +14,33 @@ pub struct FSStorage; const LOCKFILE_NAME: &str = "revision.lock"; +fn copy_with_progress(src: &Path, dst: &Path, progress: &ProgressBar) -> io::Result { + let src_file = fs::File::open(src)?; + let dst_file = fs::File::create(dst)?; + + let mut reader = BufReader::with_capacity(64 * 1024, src_file); + let mut writer = BufWriter::with_capacity(64 * 1024, dst_file); + + let mut total_bytes = 0u64; + let mut buf = [0u8; 64 * 1024]; + + loop { + let bytes_read = reader.read(&mut buf)?; + if bytes_read == 0 { + break; + } + writer.write_all(&buf[..bytes_read])?; + total_bytes += bytes_read as u64; + progress.set_position(total_bytes); + } + + writer.flush()?; + Ok(total_bytes) +} + impl StorageImpl for FSStorage { - fn get_revision(&self) -> Result { - let path = base_path().join(LOCKFILE_NAME); + fn get_revision(&self, app_state: Data) -> Result { + let path = base_path(app_state).join(LOCKFILE_NAME); let revision = match fs::read_to_string(&path) { Ok(rev) => rev.parse::().map_err(|_| Error::revision_lock())?, Err(_) => { @@ -28,21 +55,29 @@ impl StorageImpl for FSStorage { } fn store_revision(&self, app_state: Data) -> Result<()> { - let revision = self.get_revision()?; + let revision = self.get_revision(app_state.clone())?; let new_revision = revision + 1; let reporter = Reporter::new(app_state.clone()); let result = create_archive(app_state.clone(), &reporter)?; let archive_path = match &app_state.config.sync.fs_dir { Some(dir) => PathBuf::from(shellexpand::tilde(&dir).as_ref()) .join(&format!("revision_{new_revision}.tar.gz")), - _ => base_path().join(&format!("revision_{new_revision}.tar.gz")), + _ => base_path(app_state.clone()).join(&format!("revision_{new_revision}.tar.gz")), }; - result - .temp_file - .persist(&archive_path) - .map_err(|e| Error::persist_error(e, &archive_path))?; - let lockfile_path = base_path().join(LOCKFILE_NAME); + match result.temp_file.persist(&archive_path) { + Ok(_) => {} + Err(e) if e.error.raw_os_error() == Some(libc::EXDEV) => { + let src_path = e.file.path(); + let file_size = fs::metadata(src_path)?.len(); + let progress = reporter.start_bytes_progress(file_size, "copying"); + copy_with_progress(src_path, &archive_path, &progress) + .map_err(|e| Error::copy(e, &archive_path))?; + Reporter::finish_bytes_progress(&progress, "copied"); + } + Err(e) => return Err(Error::persist_error(e, &archive_path)), + } + let lockfile_path = base_path(app_state.clone()).join(LOCKFILE_NAME); fs::write(&lockfile_path, new_revision.to_string())?; tracing::info!( @@ -50,7 +85,7 @@ impl StorageImpl for FSStorage { path = ?archive_path, "stored revision" ); - let remove_path = base_path().join(&format!("revision_{revision}.tar.gz")); + let remove_path = base_path(app_state.clone()).join(&format!("revision_{revision}.tar.gz")); match fs::remove_file(&remove_path) { Ok(_) => {} Err(e) => match e.kind() { @@ -63,7 +98,16 @@ impl StorageImpl for FSStorage { } } -pub fn base_path() -> PathBuf { +pub fn base_path(app_state: Data) -> PathBuf { + match &app_state.config.sync.fs_dir { + Some(dir) => PathBuf::from(shellexpand::tilde(&dir).as_ref()), + _ => dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("minecraft-sync"), + } +} + +pub fn base_path_local() -> PathBuf { dirs::data_local_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("minecraft-sync") diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 3346af6..137c464 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use crate::{Data, error::Result}; pub trait StorageImpl { - fn get_revision(&self) -> Result; + fn get_revision(&self, app_state: Data) -> Result; fn store_revision(&self, app_state: Data) -> Result<()>; } @@ -16,9 +16,9 @@ pub enum Storage { pub const IGNORE_FILES: [&str; 6] = ["assets", "cache", "catpacks", "logs", "meta", "metacache"]; impl StorageImpl for Storage { - fn get_revision(&self) -> Result { + fn get_revision(&self, app_state: Data) -> Result { match self { - Self::FS(storage) => storage.get_revision(), + Self::FS(storage) => storage.get_revision(app_state), } }