feat!: add copying for cross fs support
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -401,6 +401,7 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"tar",
|
"tar",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ ctrlc = "3.5.1"
|
|||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
flate2 = "1.1.8"
|
flate2 = "1.1.8"
|
||||||
indicatif = "0.18.3"
|
indicatif = "0.18.3"
|
||||||
|
libc = "0.2.180"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
shellexpand = "3.1.1"
|
shellexpand = "3.1.1"
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub fn create_archive(app_state: Data, reporter: &Reporter) -> Result<ArchiveRes
|
|||||||
|
|
||||||
let temp_file = match app_state.storage {
|
let temp_file = match app_state.storage {
|
||||||
Storage::FS(_) => {
|
Storage::FS(_) => {
|
||||||
let target = crate::storage::fs::base_path();
|
let target = crate::storage::fs::base_path_local();
|
||||||
tempfile::Builder::new()
|
tempfile::Builder::new()
|
||||||
.prefix(".revision_")
|
.prefix(".revision_")
|
||||||
.suffix(".tar.gz.tmp")
|
.suffix(".tar.gz.tmp")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
storage::fs::base_path,
|
storage::fs::base_path_local,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -19,7 +19,7 @@ pub struct Sync {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let path = base_path().join("config.toml");
|
let path = base_path_local().join("config.toml");
|
||||||
let bytes = match fs::read(&path) {
|
let bytes = match fs::read(&path) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => match e.kind() {
|
Err(e) => match e.kind() {
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ pub enum ErrorKind {
|
|||||||
#[source]
|
#[source]
|
||||||
source: std::io::Error,
|
source: std::io::Error,
|
||||||
},
|
},
|
||||||
|
#[error("error copying file to {path}")]
|
||||||
|
Copy {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use tracing_subscriber::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
reporter::Reporter,
|
|
||||||
storage::{Storage, StorageImpl, get_storage_from_env},
|
storage::{Storage, StorageImpl, get_storage_from_env},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
use indicatif::{HumanBytes, ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||||
|
|
||||||
use crate::Data;
|
use crate::Data;
|
||||||
|
|
||||||
pub struct Reporter {
|
pub struct Reporter {
|
||||||
|
app_state: Data,
|
||||||
spinner: ProgressBar,
|
spinner: ProgressBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ impl Reporter {
|
|||||||
);
|
);
|
||||||
spinner.enable_steady_tick(std::time::Duration::from_millis(100));
|
spinner.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||||
spinner.set_draw_target(ProgressDrawTarget::stderr_with_hz(20));
|
spinner.set_draw_target(ProgressDrawTarget::stderr_with_hz(20));
|
||||||
Self { spinner }
|
Self { app_state, spinner }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_archive_progress(&self, total: u64) {
|
pub fn start_archive_progress(&self, total: u64) {
|
||||||
@@ -52,6 +53,39 @@ impl Reporter {
|
|||||||
self.spinner.set_style(
|
self.spinner.set_style(
|
||||||
ProgressStyle::with_template("{msg:>12.green} ✓ [{elapsed_precise}]").unwrap(),
|
ProgressStyle::with_template("{msg:>12.green} ✓ [{elapsed_precise}]").unwrap(),
|
||||||
);
|
);
|
||||||
|
self.spinner.set_prefix("");
|
||||||
self.spinner.finish_with_message(msg);
|
self.spinner.finish_with_message(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start_bytes_progress(
|
||||||
|
&self,
|
||||||
|
total_bytes: u64,
|
||||||
|
operation: impl Into<Cow<'static, str>>,
|
||||||
|
) -> 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<Cow<'static, str>>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use indicatif::ProgressBar;
|
||||||
|
|
||||||
use crate::Data;
|
use crate::Data;
|
||||||
use crate::archive::create_archive;
|
use crate::archive::create_archive;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
@@ -11,9 +14,33 @@ pub struct FSStorage;
|
|||||||
|
|
||||||
const LOCKFILE_NAME: &str = "revision.lock";
|
const LOCKFILE_NAME: &str = "revision.lock";
|
||||||
|
|
||||||
|
fn copy_with_progress(src: &Path, dst: &Path, progress: &ProgressBar) -> io::Result<u64> {
|
||||||
|
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 {
|
impl StorageImpl for FSStorage {
|
||||||
fn get_revision(&self) -> Result<usize> {
|
fn get_revision(&self, app_state: Data) -> Result<usize> {
|
||||||
let path = base_path().join(LOCKFILE_NAME);
|
let path = base_path(app_state).join(LOCKFILE_NAME);
|
||||||
let revision = match fs::read_to_string(&path) {
|
let revision = match fs::read_to_string(&path) {
|
||||||
Ok(rev) => rev.parse::<usize>().map_err(|_| Error::revision_lock())?,
|
Ok(rev) => rev.parse::<usize>().map_err(|_| Error::revision_lock())?,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -28,21 +55,29 @@ impl StorageImpl for FSStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn store_revision(&self, app_state: Data) -> Result<()> {
|
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 new_revision = revision + 1;
|
||||||
let reporter = Reporter::new(app_state.clone());
|
let reporter = Reporter::new(app_state.clone());
|
||||||
let result = create_archive(app_state.clone(), &reporter)?;
|
let result = create_archive(app_state.clone(), &reporter)?;
|
||||||
let archive_path = match &app_state.config.sync.fs_dir {
|
let archive_path = match &app_state.config.sync.fs_dir {
|
||||||
Some(dir) => PathBuf::from(shellexpand::tilde(&dir).as_ref())
|
Some(dir) => PathBuf::from(shellexpand::tilde(&dir).as_ref())
|
||||||
.join(&format!("revision_{new_revision}.tar.gz")),
|
.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
|
match result.temp_file.persist(&archive_path) {
|
||||||
.temp_file
|
Ok(_) => {}
|
||||||
.persist(&archive_path)
|
Err(e) if e.error.raw_os_error() == Some(libc::EXDEV) => {
|
||||||
.map_err(|e| Error::persist_error(e, &archive_path))?;
|
let src_path = e.file.path();
|
||||||
let lockfile_path = base_path().join(LOCKFILE_NAME);
|
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())?;
|
fs::write(&lockfile_path, new_revision.to_string())?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -50,7 +85,7 @@ impl StorageImpl for FSStorage {
|
|||||||
path = ?archive_path,
|
path = ?archive_path,
|
||||||
"stored revision"
|
"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) {
|
match fs::remove_file(&remove_path) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => match e.kind() {
|
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()
|
dirs::data_local_dir()
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
.join("minecraft-sync")
|
.join("minecraft-sync")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::path::PathBuf;
|
|||||||
use crate::{Data, error::Result};
|
use crate::{Data, error::Result};
|
||||||
|
|
||||||
pub trait StorageImpl {
|
pub trait StorageImpl {
|
||||||
fn get_revision(&self) -> Result<usize>;
|
fn get_revision(&self, app_state: Data) -> Result<usize>;
|
||||||
fn store_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"];
|
pub const IGNORE_FILES: [&str; 6] = ["assets", "cache", "catpacks", "logs", "meta", "metacache"];
|
||||||
|
|
||||||
impl StorageImpl for Storage {
|
impl StorageImpl for Storage {
|
||||||
fn get_revision(&self) -> Result<usize> {
|
fn get_revision(&self, app_state: Data) -> Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Self::FS(storage) => storage.get_revision(),
|
Self::FS(storage) => storage.get_revision(app_state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user