feat: Rewrote the system architecture (#5)

* refactor: Remove file locking functionality and associated references

* refactor: Replace PathBuf with Path references across interfaces and remove redundant implementations

* fix: UI missing "Comparison Mode" field, and set permission failed

* refactor: Restructure core module into submodules for better organization

* wip: Remove event system

* refactor: Introduce actor model framework and reorganize module hierarchy

* wip: Remove event system and restructure module hierarchy for actor-based architecture

* refactor: Remove unused traits and modules, enhance GUI handling, and refine actor-driven architecture
This commit is contained in:
DaLaw2 2025-08-15 23:05:57 +08:00 committed by GitHub
parent 9c16e05c8e
commit fe5f8dd258
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 1725 additions and 1219 deletions

5
.gitignore vendored
View File

@ -1,5 +1,6 @@
target
logs
.idea
logs
target
devnote.txt
mirrorSphere.db
db.lock

View File

@ -5,8 +5,8 @@ fn main() {
.set("InternalName", "MirrorSphere.exe")
.set_version_info(winres::VersionInfo::PRODUCTVERSION, 0x0001000000000000)
.set_language(0x0409);
if let Err(e) = res.compile() {
eprintln!("winres error: {}", e);
if let Err(err) = res.compile() {
eprintln!("winres error: {err}");
}
}

View File

@ -1,21 +1,20 @@
use crate::core::app_config::AppConfig;
use crate::core::event_bus::EventBus;
use crate::core::io_manager::IOManager;
use crate::core::progress_tracker::ProgressTracker;
use crate::core::backup::progress_tracker::ProgressTracker;
use crate::core::gui::gui_message_handler::GuiMessageHandler;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::io_manager::IOManager;
use crate::interface::file_system::FileSystemTrait;
use crate::interface::service_unit::ServiceUnit;
use crate::model::backup::backup_execution::*;
use crate::model::error::Error;
use crate::model::core::backup::backup_execution::*;
use crate::model::core::gui::message::GuiMessage;
use crate::model::error::system::SystemError;
use crate::model::error::task::TaskError;
use crate::model::event::execution::*;
use async_trait::async_trait;
use crate::model::error::Error;
use crossbeam_queue::SegQueue;
use dashmap::DashMap;
use futures::future::join_all;
use macros::log;
use std::collections::{HashSet, VecDeque};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
@ -24,8 +23,8 @@ use uuid::Uuid;
pub struct BackupEngine {
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
io_manager: Arc<IOManager>,
actor_system: Arc<ActorSystem>,
progress_tracker: Arc<ProgressTracker>,
executions: Arc<DashMap<Uuid, BackupExecution>>,
running_executions: Arc<DashMap<Uuid, (oneshot::Sender<()>, JoinHandle<()>)>>,
@ -34,14 +33,14 @@ pub struct BackupEngine {
impl BackupEngine {
pub fn new(
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
io_manager: Arc<IOManager>,
actor_system: Arc<ActorSystem>,
progress_tracker: Arc<ProgressTracker>,
) -> Self {
Self {
app_config,
event_bus,
io_manager,
actor_system,
progress_tracker,
executions: Arc::new(DashMap::new()),
running_executions: Arc::new(DashMap::new()),
@ -52,7 +51,7 @@ impl BackupEngine {
let keys: Vec<Uuid> = self
.running_executions
.iter()
.map(|pair| pair.key().clone())
.map(|pair| *pair.key())
.collect();
for uuid in keys {
if let Some((_, (shutdown, handle))) = self.running_executions.remove(&uuid) {
@ -67,10 +66,6 @@ impl BackupEngine {
}
}
pub fn get_execution(&self, uuid: &Uuid) -> Option<BackupExecution> {
self.executions.get(uuid).map(|entry| entry.clone())
}
pub fn get_all_executions(&self) -> Vec<(Uuid, BackupExecution)> {
self.executions
.iter()
@ -158,12 +153,14 @@ impl BackupEngine {
fn to_execution_runner(&self) -> ExecutionRunner {
let config = self.app_config.clone();
let io_manager = self.io_manager.clone();
let actor_system = self.actor_system.clone();
let progress_tracker = self.progress_tracker.clone();
let executions = self.executions.clone();
let running_executions = self.running_executions.clone();
ExecutionRunner::new(
config,
io_manager,
actor_system,
progress_tracker,
executions,
running_executions,
@ -174,6 +171,7 @@ impl BackupEngine {
struct ExecutionRunner {
app_config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
actor_system: Arc<ActorSystem>,
progress_tracker: Arc<ProgressTracker>,
executions: Arc<DashMap<Uuid, BackupExecution>>,
running_executions: Arc<DashMap<Uuid, (oneshot::Sender<()>, JoinHandle<()>)>>,
@ -183,6 +181,7 @@ impl ExecutionRunner {
pub fn new(
app_config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
actor_system: Arc<ActorSystem>,
progress_tracker: Arc<ProgressTracker>,
executions: Arc<DashMap<Uuid, BackupExecution>>,
running_executions: Arc<DashMap<Uuid, (oneshot::Sender<()>, JoinHandle<()>)>>,
@ -190,6 +189,7 @@ impl ExecutionRunner {
Self {
app_config,
io_manager,
actor_system,
progress_tracker,
executions,
running_executions,
@ -251,7 +251,21 @@ impl ExecutionRunner {
match result {
Ok((worker_next_level, worker_errors)) => {
next_level.extend(worker_next_level);
errors.extend(worker_errors);
if !worker_errors.is_empty() {
errors.extend(worker_errors.clone());
if let Some(gui_ref) = self.actor_system.actor_of::<GuiMessageHandler>()
{
if let Err(err) = gui_ref
.tell(GuiMessage::ExecutionErrors {
uuid: execution.uuid,
errors: worker_errors,
})
.await
{
error!("{}", err);
}
}
}
}
Err(err) => log!(SystemError::ThreadPanic(err)),
}
@ -261,7 +275,8 @@ impl ExecutionRunner {
current_level.extend(next_level);
if let Err(err) = progress_tracker
.save_execution(execution.uuid, current_level, errors)
.await {
.await
{
error!("{}", err);
}
break;
@ -297,9 +312,7 @@ struct Worker {
impl Worker {
pub fn new(io_manager: Arc<IOManager>) -> Self {
Self {
io_manager
}
Self { io_manager }
}
async fn run(
@ -370,30 +383,30 @@ impl Worker {
async fn process_entry(
&self,
execution: &BackupExecution,
current_path: &PathBuf,
current_path: &Path,
) -> Result<Option<PathBuf>, Error> {
let io_manager = &self.io_manager;
let source_root = &execution.source_path;
let destination_root = &execution.destination_path;
let source_path = current_path.clone();
let destination_path =
self.calculate_destination_path(&source_path, &source_root, &destination_root)?;
let source_path = current_path;
let destination_path = self.calculate_destination_path(source_path, source_root, destination_root)?;
let destination_path = destination_path.as_path();
let is_symlink = io_manager.is_symlink(&source_path).await.unwrap_or(false);
let is_symlink = io_manager.is_symlink(source_path).await.unwrap_or(false);
if is_symlink {
self.process_symlink(execution, &source_path, &destination_path)
self.process_symlink(execution, source_path, destination_path)
.await?;
return Ok(None);
}
if source_path.is_dir() {
self.backup_directory(execution, &source_path, &destination_path)
self.backup_directory(execution, source_path, destination_path)
.await
} else {
self.backup_file(execution, &source_path, &destination_path)
self.backup_file(execution, source_path, destination_path)
.await
}
}
@ -401,13 +414,13 @@ impl Worker {
async fn backup_directory(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
source_path: &Path,
destination_path: &Path,
) -> Result<Option<PathBuf>, Error> {
let io_manager = &self.io_manager;
if !destination_path.exists() {
io_manager.create_directory(&destination_path).await?;
io_manager.create_directory(destination_path).await?;
}
io_manager
@ -420,24 +433,17 @@ impl Worker {
.await?;
}
Ok(Some(source_path.clone()))
Ok(Some(source_path.to_path_buf()))
}
async fn backup_file(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
source_path: &Path,
destination_path: &Path,
) -> Result<Option<PathBuf>, Error> {
let io_manager = &self.io_manager;
#[allow(unused_variables)]
let mut file_lock = None;
#[allow(unused_assignments)]
if execution.options.lock_source {
file_lock = Some(io_manager.acquire_file_lock(source_path).await?);
}
match execution.backup_type {
BackupType::Full => self.full_backup(source_path, destination_path).await?,
BackupType::Incremental => {
@ -457,8 +463,6 @@ impl Worker {
.await?;
}
drop(file_lock);
Ok(None)
}
@ -466,8 +470,8 @@ impl Worker {
async fn process_symlink(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
source_path: &Path,
destination_path: &Path,
) -> Result<(), Error> {
if execution.options.follow_symlinks {
self.follow_symlink(execution, source_path, destination_path)
@ -481,15 +485,15 @@ impl Worker {
async fn follow_symlink(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
source_path: &Path,
destination_path: &Path,
) -> Result<(), Error> {
let io_manager = &self.io_manager;
let mut queue = VecDeque::new();
let mut visited = HashSet::new();
queue.push_back((source_path.clone(), destination_path.clone()));
queue.push_back((source_path.to_path_buf(), destination_path.to_path_buf()));
while let Some((current_source, current_dest)) = queue.pop_front() {
let is_symlink = io_manager
@ -502,7 +506,7 @@ impl Worker {
Err(_) => continue,
}
} else {
current_source.clone()
current_source.to_path_buf()
};
if visited.contains(&canonical_path) {
@ -538,8 +542,8 @@ impl Worker {
async fn copy_symlink(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
source_path: &Path,
destination_path: &Path,
) -> Result<(), Error> {
let io_manager = &self.io_manager;
@ -561,19 +565,15 @@ impl Worker {
}
#[inline(always)]
async fn full_backup(
&self,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> Result<(), Error> {
async fn full_backup(&self, source_path: &Path, destination_path: &Path) -> Result<(), Error> {
let io_manager = &self.io_manager;
io_manager.copy_file(source_path, destination_path).await
}
async fn incremental_backup(
&self,
source_path: &PathBuf,
destination_path: &PathBuf,
source_path: &Path,
destination_path: &Path,
comparison_mode: ComparisonMode,
) -> Result<(), Error> {
let io_manager = &self.io_manager;
@ -636,9 +636,9 @@ impl Worker {
fn calculate_destination_path(
&self,
source_path: &PathBuf,
source_root: &PathBuf,
destination_root: &PathBuf,
source_path: &Path,
source_root: &Path,
destination_root: &Path,
) -> Result<PathBuf, Error> {
let relative_path = source_path
.strip_prefix(source_root)
@ -646,43 +646,3 @@ impl Worker {
Ok(destination_root.join(relative_path))
}
}
#[async_trait]
impl ServiceUnit for BackupEngine {
async fn run_impl(self: Arc<Self>, mut shutdown_rx: oneshot::Receiver<()>) {
let backup_engine = self.clone();
let event_bus = self.event_bus.clone();
let add_execution = event_bus.subscribe::<ExecutionAddRequest>();
let remove_execution = event_bus.subscribe::<ExecutionRemoveRequest>();
let start_execution = event_bus.subscribe::<ExecutionStartRequest>();
let resume_execution = event_bus.subscribe::<ExecutionResumeRequested>();
let suspend_execution = event_bus.subscribe::<ExecutionSuspendRequest>();
loop {
if shutdown_rx.try_recv().is_ok() {
break;
}
while let Ok(event) = add_execution.try_recv() {
backup_engine.add_execution(event.execution).await;
}
while let Ok(event) = remove_execution.try_recv() {
backup_engine.remove_execution(&event.execution_id).await;
}
while let Ok(event) = start_execution.try_recv() {
if let Err(err) = backup_engine.start_execution(event.execution_id).await {
error!("{}", err);
}
}
while let Ok(event) = resume_execution.try_recv() {
if let Err(err) = backup_engine.resume_execution(event.execution_id).await {
error!("{}", err);
}
}
while let Ok(event) = suspend_execution.try_recv() {
if let Err(err) = backup_engine.suspend_execution(event.execution_id).await {
error!("{}", err);
}
}
}
}
}

View File

@ -0,0 +1,82 @@
use crate::core::backup::backup_engine::BackupEngine;
use crate::core::backup::progress_tracker::ProgressTracker;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::io_manager::IOManager;
use crate::interface::actor::actor::Actor;
use crate::interface::actor::message::Message;
use crate::model::core::backup::message::*;
use crate::model::error::Error;
use async_trait::async_trait;
use std::sync::Arc;
pub struct BackupService {
backup_engine: Arc<BackupEngine>,
}
impl BackupService {
pub async fn init(
app_config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
actor_system: Arc<ActorSystem>,
) {
let progress_tracker = Arc::new(ProgressTracker::new(io_manager.clone()));
let backup_engine = Arc::new(BackupEngine::new(
app_config,
io_manager,
actor_system.clone(),
progress_tracker,
));
let backup_service = Self {
backup_engine,
};
actor_system.spawn(backup_service).await;
}
}
#[async_trait]
impl Actor for BackupService {
type Message = BackupServiceMessage;
async fn pre_start(&mut self) {}
async fn post_stop(&mut self) {
self.backup_engine.stop_all_executions().await;
}
async fn receive(
&mut self,
message: Self::Message,
) -> Result<<Self::Message as Message>::Response, Error> {
match message {
BackupServiceMessage::ServiceCall(service_call) => match service_call {
ServiceCallMessage::AddExecution(execution) => {
self.backup_engine.add_execution(execution).await;
Ok(BackupServiceResponse::None)
}
ServiceCallMessage::RemoveExecution(uuid) => {
self.backup_engine.resume_execution(uuid).await?;
Ok(BackupServiceResponse::None)
}
ServiceCallMessage::StartExecution(uuid) => {
self.backup_engine.start_execution(uuid).await?;
Ok(BackupServiceResponse::None)
}
ServiceCallMessage::SuspendExecution(uuid) => {
self.backup_engine.suspend_execution(uuid).await?;
Ok(BackupServiceResponse::None)
}
ServiceCallMessage::ResumeExecution(uuid) => {
self.backup_engine.resume_execution(uuid).await?;
Ok(BackupServiceResponse::None)
}
ServiceCallMessage::GetExecutions => {
let execution = self.backup_engine.get_all_executions();
Ok(BackupServiceResponse::ServiceCall(
ServiceCallResponse::GetExecutions(execution),
))
}
},
}
}
}

3
src/core/backup/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod backup_engine;
pub mod progress_tracker;
pub mod backup_service;

View File

@ -1,6 +1,6 @@
use crate::core::io_manager::IOManager;
use crate::core::infrastructure::io_manager::IOManager;
use crate::interface::file_system::FileSystemTrait;
use crate::model::backup::progress_data::ProgressData;
use crate::model::core::backup::progress_data::ProgressData;
use crate::model::error::Error;
use crate::model::error::io::IOError;
use crate::model::error::misc::MiscError;

View File

@ -1,44 +0,0 @@
use crate::interface::event::Event;
use dashmap::DashMap;
use std::any::{Any, TypeId};
use std::sync::mpsc::{channel, Receiver};
pub struct EventBus {
channels: DashMap<TypeId, Vec<Box<dyn Fn(&dyn Any) + Send + Sync>>>,
}
impl EventBus {
pub fn new() -> Self {
Self {
channels: DashMap::new(),
}
}
pub fn subscribe<E: Event>(&self) -> Receiver<E> {
let (tx, rx) = channel();
let type_id = TypeId::of::<E>();
let handler = Box::new(move |event: &dyn Any| {
if let Some(typed_event) = event.downcast_ref::<E>() {
let _ = tx.send(typed_event.clone());
}
});
self.channels
.entry(type_id)
.or_default()
.push(handler);
rx
}
#[allow(dead_code)]
pub fn publish<E: Event>(&self, event: E) {
let type_id = TypeId::of::<E>();
if let Some(handlers) = self.channels.get(&type_id) {
for handler in handlers.value() {
handler(&event);
}
}
}
}

View File

@ -0,0 +1,61 @@
use crate::core::gui::gui_message_handler::GuiMessageHandler;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::model::error::misc::MiscError;
use crate::model::error::Error;
use crate::ui::execution_page::ExecutionPage;
use crate::ui::main_page::MainPage;
use crate::ui::schedule_page::SchedulePage;
use crate::utils::assets::Assets;
use crate::utils::font;
use eframe::egui;
use std::sync::Arc;
pub struct GuiManager {
app_config: Arc<AppConfig>,
actor_system: Arc<ActorSystem>,
}
impl GuiManager {
pub fn new(app_config: Arc<AppConfig>, actor_system: Arc<ActorSystem>) -> Self {
Self {
app_config,
actor_system,
}
}
pub async fn start(&self) -> Result<(), Error> {
let app_config = self.app_config.clone();
let actor_system = self.actor_system.clone();
let mut handler = GuiMessageHandler::new();
let message_rx = handler.subscribe();
actor_system.spawn(handler).await;
let execution_page =
ExecutionPage::new(app_config.clone(), actor_system.clone(), message_rx);
let schedule_page = SchedulePage::new(app_config, actor_system)?;
let main_page = MainPage::new(execution_page, schedule_page);
let icon_data = Assets::load_app_icon()?;
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([960.0, 540.0])
.with_title("MirrorSphere")
.with_icon(icon_data),
..Default::default()
};
eframe::run_native(
"MirrorSphere",
options,
Box::new(|cc| {
font::setup_system_fonts(&cc.egui_ctx);
Ok(Box::new(main_page))
}),
)
.map_err(MiscError::UIPlatformError)?;
Ok(())
}
}

View File

@ -0,0 +1,48 @@
use crate::interface::actor::actor::Actor;
use crate::interface::actor::message::Message;
use crate::model::core::gui::message::GuiMessage;
use crate::model::error::Error;
use async_trait::async_trait;
use std::sync::mpsc;
pub struct GuiMessageHandler {
subscriber: Vec<mpsc::Sender<GuiMessage>>,
}
impl GuiMessageHandler {
pub fn new() -> Self {
Self {
subscriber: Vec::new(),
}
}
pub fn subscribe(&mut self) -> mpsc::Receiver<GuiMessage> {
let (tx, rx) = mpsc::channel();
self.subscriber.push(tx);
rx
}
fn broadcast(&mut self, message: GuiMessage) {
self.subscriber
.retain(|tx| tx.send(message.clone()).is_ok());
}
}
#[async_trait]
impl Actor for GuiMessageHandler {
type Message = GuiMessage;
async fn pre_start(&mut self) {}
async fn post_stop(&mut self) {
self.subscriber.clear();
}
async fn receive(
&mut self,
message: Self::Message,
) -> Result<<Self::Message as Message>::Response, Error> {
self.broadcast(message);
Ok(())
}
}

2
src/core/gui/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod gui_message_handler;
pub mod gui_manager;

View File

@ -1,68 +0,0 @@
use crate::core::app_config::AppConfig;
use crate::core::backup_engine::BackupEngine;
use crate::core::event_bus::EventBus;
use crate::core::schedule_manager::ScheduleManager;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
use crate::ui::main_page::MainPage;
use crate::utils::assets::Assets;
use crate::utils::font;
use eframe::egui;
use std::sync::Arc;
pub struct GuiManager {
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
schedule_manager: Arc<ScheduleManager>,
}
impl GuiManager {
pub fn new(
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
schedule_manager: Arc<ScheduleManager>,
) -> Self {
Self {
app_config,
event_bus,
backup_engine,
schedule_manager,
}
}
pub fn start(&self) -> Result<(), Error> {
let config = self.app_config.clone();
let event_bus = self.event_bus.clone();
let backup_engine = self.backup_engine.clone();
let schedule_manager = self.schedule_manager.clone();
let icon_data = Assets::load_app_icon()?;
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([800.0, 450.0])
.with_title("MirrorSphere")
.with_icon(icon_data),
..Default::default()
};
eframe::run_native(
"MirrorSphere",
options,
Box::new(|cc| {
font::setup_system_fonts(&cc.egui_ctx);
Ok(Box::new(MainPage::new(
config,
event_bus,
backup_engine,
schedule_manager,
)))
}),
)
.map_err(MiscError::UIPlatformError)?;
Ok(())
}
}

View File

@ -0,0 +1,56 @@
use crate::interface::actor::actor::Actor;
use crate::model::core::actor::actor_ref::ActorRef;
use crate::model::core::actor::actor_runtime::ActorRuntime;
use crate::model::error::system::SystemError;
use dashmap::DashMap;
use macros::log;
use std::any::{Any, TypeId};
use tokio::sync::oneshot;
pub struct ActorSystem {
actors: DashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>,
shutdowns: DashMap<TypeId, oneshot::Sender<()>>,
}
impl ActorSystem {
pub fn new() -> Self {
Self {
actors: DashMap::new(),
shutdowns: DashMap::new(),
}
}
pub async fn spawn<A>(&self, actor: A)
where
A: Actor + 'static,
{
let actor_id = TypeId::of::<A>();
let (actor_runtime, actor_ref) = ActorRuntime::new(actor);
let shutdown = actor_runtime.run().await;
self.actors.insert(actor_id, Box::new(actor_ref));
self.shutdowns.insert(actor_id, shutdown);
}
pub fn shutdown(&self) {
let keys = self
.shutdowns
.iter()
.map(|x| x.key().clone())
.collect::<Vec<_>>();
for key in keys {
if let Some((_, shutdown)) = self.shutdowns.remove(&key) {
if let Err(_) = shutdown.send(()) {
log!(SystemError::ShutdownSignalFailed);
}
}
}
}
pub fn actor_of<A: Actor>(&self) -> Option<ActorRef<A::Message>> {
let type_id = TypeId::of::<A>();
self.actors
.get(&type_id)?
.downcast_ref::<ActorRef<A::Message>>()
.cloned()
}
}

View File

@ -20,7 +20,7 @@ impl AppConfig {
fn load_config_file() -> Result<Config, Error> {
let toml_string =
fs::read_to_string("./config.toml").map_err(SystemError::ConfigNotFound)?;
fs::read_to_string("config.toml").map_err(SystemError::ConfigNotFound)?;
let config = toml::from_str::<ConfigTable>(&toml_string)
.map_err(SystemError::InvalidConfig)?
.config;

View File

@ -1,9 +1,10 @@
use crate::interface::repository::schedule::ScheduleRepository;
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
use crate::model::error::Error;
use crate::model::log::database::DatabaseLog;
use crate::model::log::system::SystemLog;
use crate::platform::constants::*;
use crate::utils::database_lock::DatabaseLock;
use macros::log;
use sqlx::SqlitePool;
use tokio::fs;
@ -12,11 +13,13 @@ use tokio::fs::File;
#[derive(Debug)]
pub struct DatabaseManager {
pool: SqlitePool,
_lock: DatabaseLock,
}
impl DatabaseManager {
pub async fn new() -> Result<Self, Error> {
log!(SystemLog::Initializing);
let lock = DatabaseLock::acquire().await?;
if !Self::exist_database().await {
Self::create_database().await?;
}
@ -24,7 +27,7 @@ impl DatabaseManager {
.await
.map_err(DatabaseError::DatabaseConnectFailed)?;
log!(DatabaseLog::DatabaseConnectSuccess);
let database_manager = Self { pool };
let database_manager = Self { pool, _lock: lock };
if !database_manager.exist_table("BackupSchedules").await {
database_manager.create_backup_schedule_table().await?;
}
@ -36,11 +39,6 @@ impl DatabaseManager {
self.pool.clone()
}
pub async fn close_connection(&self) {
let pool = self.get_pool();
pool.close().await
}
pub async fn exist_database() -> bool {
fs::metadata(DATABASE_PATH).await.is_ok()
}

View File

@ -1,4 +1,4 @@
use crate::core::app_config::AppConfig;
use crate::core::infrastructure::app_config::AppConfig;
use crate::interface::file_system::FileSystemTrait;
use crate::platform::file_system::FileSystem;
use std::ops::Deref;
@ -17,10 +17,6 @@ impl IOManager {
file_system: FileSystem::new(semaphore),
}
}
pub fn terminate(&self) {
self.file_system.semaphore().close();
}
}
impl Deref for IOManager {
@ -30,3 +26,9 @@ impl Deref for IOManager {
&self.file_system
}
}
impl Drop for IOManager {
fn drop(&mut self) {
self.file_system.semaphore().close();
}
}

View File

@ -0,0 +1,4 @@
pub mod app_config;
pub mod database_manager;
pub mod io_manager;
pub mod actor_system;

View File

@ -1,9 +1,5 @@
pub mod app_config;
pub mod backup_engine;
pub mod database_manager;
pub mod event_bus;
pub mod gui_manager;
pub mod io_manager;
pub mod progress_tracker;
pub mod schedule_manager;
pub mod backup;
pub mod gui;
pub mod infrastructure;
pub mod schedule;
pub mod system;

3
src/core/schedule/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod schedule_manager;
pub mod schedule_service;
pub mod schedule_timer;

View File

@ -0,0 +1,145 @@
use crate::core::backup::backup_service::BackupService;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::database_manager::DatabaseManager;
use crate::interface::repository::schedule::ScheduleRepository;
use crate::model::core::backup::message::{BackupServiceMessage, ServiceCallMessage};
use crate::model::core::schedule::backup_schedule::*;
use crate::model::error::Error;
use chrono::{Duration, Months, Utc};
use dashmap::DashMap;
use std::sync::Arc;
use uuid::Uuid;
pub struct ScheduleManager {
database_manager: Arc<DatabaseManager>,
actor_system: Arc<ActorSystem>,
schedules: DashMap<Uuid, BackupSchedule>,
}
impl ScheduleManager {
pub async fn new(
database_manager: Arc<DatabaseManager>,
actor_system: Arc<ActorSystem>,
) -> Result<Self, Error> {
let schedules = DashMap::new();
let database_schedules = database_manager.get_all_backup_schedules().await?;
for schedule in database_schedules {
schedules.insert(schedule.uuid, schedule);
}
let schedule_manager = ScheduleManager {
database_manager,
actor_system,
schedules,
};
Ok(schedule_manager)
}
pub async fn get_all_schedules(&self) -> Vec<BackupSchedule> {
self.schedules.iter().map(|x| x.value().clone()).collect()
}
pub async fn create_schedule(&self, schedule: BackupSchedule) -> Result<(), Error> {
self.database_manager
.create_backup_schedule(&schedule)
.await?;
self.schedules.insert(schedule.uuid, schedule);
Ok(())
}
pub async fn modify_schedule(&self, schedule: BackupSchedule) -> Result<(), Error> {
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
self.schedules.insert(schedule.uuid, schedule);
Ok(())
}
pub async fn remove_schedule(&self, uuid: Uuid) -> Result<(), Error> {
self.database_manager.remove_backup_schedule(uuid).await?;
self.schedules.remove(&uuid);
Ok(())
}
pub async fn active_schedule(&self, uuid: Uuid) -> Result<(), Error> {
if let Some(mut schedule) = self.database_manager.get_backup_schedule(uuid).await? {
schedule.state = ScheduleState::Active;
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
self.schedules.insert(schedule.uuid, schedule);
}
Ok(())
}
pub async fn pause_schedule(&self, uuid: Uuid) -> Result<(), Error> {
if let Some(mut schedule) = self.database_manager.get_backup_schedule(uuid).await? {
schedule.state = ScheduleState::Paused;
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
self.schedules.insert(schedule.uuid, schedule);
}
Ok(())
}
pub async fn disable_schedule(&self, uuid: Uuid) -> Result<(), Error> {
if let Some(mut schedule) = self.database_manager.get_backup_schedule(uuid).await? {
schedule.state = ScheduleState::Disabled;
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
self.schedules.insert(schedule.uuid, schedule);
}
Ok(())
}
pub async fn execute_ready_schedule(&self) -> Result<(), Error> {
let database_manager = self.database_manager.clone();
let now = Utc::now().naive_utc();
let mut schedules = self.get_all_schedules().await;
for schedule in schedules.iter_mut() {
if schedule.state != ScheduleState::Active {
continue;
}
if let Some(next_run_time) = schedule.next_run_time {
if next_run_time >= now {
continue;
}
let execution = schedule.to_execution();
if let Some(service_ref) = self.actor_system.actor_of::<BackupService>() {
service_ref
.tell(BackupServiceMessage::ServiceCall(
ServiceCallMessage::AddExecution(execution),
))
.await?;
}
self.update_next_run_time(schedule);
database_manager.modify_backup_schedule(schedule).await?;
}
}
Ok(())
}
fn update_next_run_time(&self, schedule: &mut BackupSchedule) {
if schedule.next_run_time.is_none() {
return;
}
let now = Utc::now().naive_utc();
let old_next_run_time = schedule.next_run_time.unwrap();
let new_next_run_time = match schedule.interval {
ScheduleInterval::Once => None,
ScheduleInterval::Daily => Some(old_next_run_time + Duration::days(1)),
ScheduleInterval::Weekly => Some(old_next_run_time + Duration::days(7)),
ScheduleInterval::Monthly => Some(
old_next_run_time
.checked_add_months(Months::new(1))
.unwrap_or(old_next_run_time + Duration::days(30)),
),
};
schedule.last_run_time = Some(now);
schedule.next_run_time = new_next_run_time;
}
}

View File

@ -0,0 +1,123 @@
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::database_manager::DatabaseManager;
use crate::core::schedule::schedule_manager::ScheduleManager;
use crate::core::schedule::schedule_timer::ScheduleTimer;
use crate::interface::actor::actor::Actor;
use crate::interface::actor::message::Message;
use crate::model::core::schedule::message::*;
use crate::model::error::system::SystemError;
use crate::model::error::Error;
use async_trait::async_trait;
use macros::log;
use std::sync::{Arc, OnceLock};
use tokio::sync::{mpsc, oneshot};
pub struct ScheduleService {
schedule_manager: Arc<ScheduleManager>,
schedule_timer: Arc<ScheduleTimer>,
timer_refresh: OnceLock<mpsc::UnboundedSender<()>>,
shutdowns: Vec<oneshot::Sender<()>>,
}
impl ScheduleService {
pub async fn init(
app_config: Arc<AppConfig>,
database_manager: Arc<DatabaseManager>,
actor_system: Arc<ActorSystem>,
) -> Result<(), Error> {
let schedule_manager =
Arc::new(ScheduleManager::new(database_manager, actor_system.clone()).await?);
let schedule_timer = Arc::new(ScheduleTimer::new(app_config, actor_system.clone()));
let schedule_service = Self {
schedule_manager,
schedule_timer,
timer_refresh: OnceLock::new(),
shutdowns: Vec::new(),
};
actor_system.spawn(schedule_service).await;
Ok(())
}
pub fn refresh_timer(&self) {
if let Some(timer_refresh) = self.timer_refresh.get() {
let _ = timer_refresh.send(());
}
}
}
#[async_trait]
impl Actor for ScheduleService {
type Message = ScheduleServiceMessage;
async fn pre_start(&mut self) {
let schedule_timer = self.schedule_timer.clone();
if let Ok((timer_refresh, shutdown)) = schedule_timer.run().await {
self.timer_refresh.get_or_init(|| timer_refresh);
self.shutdowns.push(shutdown);
}
}
async fn post_stop(&mut self) {
let shutdowns = std::mem::take(&mut self.shutdowns);
for shutdown in shutdowns {
if let Err(_) = shutdown.send(()) {
log!(SystemError::ShutdownSignalFailed)
}
}
}
async fn receive(
&mut self,
message: Self::Message,
) -> Result<<Self::Message as Message>::Response, Error> {
match message {
ScheduleServiceMessage::UnitNotification(unit_notification) => {
match unit_notification {
UnitNotificationMessage::CheckSchedule => {
self.schedule_manager.execute_ready_schedule().await?;
Ok(ScheduleServiceResponse::None)
}
}
}
ScheduleServiceMessage::ServiceCall(service_call) => match service_call {
ServiceCallMessage::AddSchedule(schedule) => {
self.schedule_manager.create_schedule(schedule).await?;
self.refresh_timer();
Ok(ScheduleServiceResponse::None)
}
ServiceCallMessage::ModifySchedule(schedule) => {
self.schedule_manager.modify_schedule(schedule).await?;
self.refresh_timer();
Ok(ScheduleServiceResponse::None)
}
ServiceCallMessage::RemoveSchedule(uuid) => {
self.schedule_manager.remove_schedule(uuid).await?;
self.refresh_timer();
Ok(ScheduleServiceResponse::None)
}
ServiceCallMessage::ActivateSchedule(uuid) => {
self.schedule_manager.active_schedule(uuid).await?;
self.refresh_timer();
Ok(ScheduleServiceResponse::None)
}
ServiceCallMessage::PauseSchedule(uuid) => {
self.schedule_manager.pause_schedule(uuid).await?;
self.refresh_timer();
Ok(ScheduleServiceResponse::None)
}
ServiceCallMessage::DisableSchedule(uuid) => {
self.schedule_manager.disable_schedule(uuid).await?;
self.refresh_timer();
Ok(ScheduleServiceResponse::None)
}
ServiceCallMessage::GetSchedules => {
let schedules = self.schedule_manager.get_all_schedules().await;
Ok(ScheduleServiceResponse::ServiceCall(
ServiceCallResponse::GetSchedules(schedules),
))
}
},
}
}
}

View File

@ -0,0 +1,107 @@
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::schedule::schedule_service::ScheduleService;
use crate::model::core::schedule::backup_schedule::ScheduleState;
use crate::model::core::schedule::message::*;
use crate::model::error::actor::ActorError;
use crate::model::error::Error;
use chrono::{Duration, Utc};
use std::sync::Arc;
use tokio::select;
use tokio::sync::{mpsc, oneshot};
use tokio::time::sleep;
use tracing::error;
pub struct ScheduleTimer {
app_config: Arc<AppConfig>,
actor_system: Arc<ActorSystem>,
}
impl ScheduleTimer {
pub fn new(app_config: Arc<AppConfig>, actor_system: Arc<ActorSystem>) -> Self {
ScheduleTimer {
app_config,
actor_system,
}
}
pub async fn run(
self: Arc<Self>,
) -> Result<(mpsc::UnboundedSender<()>, oneshot::Sender<()>), Error> {
let (refresh_tx, mut refresh_rx) = mpsc::unbounded_channel();
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
let service_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
tokio::spawn(async move {
loop {
let mut sleep_time = match self.calculate_sleep_duration().await {
Ok(Some(duration)) => duration,
Ok(None) => Duration::seconds(self.app_config.default_wakeup_time),
Err(err) => {
error!("{}", err);
Duration::seconds(self.app_config.default_wakeup_time)
}
};
if sleep_time < Duration::seconds(0) {
sleep_time = Duration::seconds(0);
}
select! {
biased;
_ = &mut shutdown_rx => { break; }
_ = refresh_rx.recv() => { continue; }
_ = sleep(sleep_time.to_std().unwrap()) => {}
}
if let Err(err) = service_ref
.tell(ScheduleServiceMessage::UnitNotification(
UnitNotificationMessage::CheckSchedule,
))
.await
{
error!("{}", err);
}
}
});
Ok((refresh_tx, shutdown_tx))
}
async fn calculate_sleep_duration(&self) -> Result<Option<Duration>, Error> {
let mut next_time = None;
let service_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let response = service_ref
.ask(ScheduleServiceMessage::ServiceCall(
ServiceCallMessage::GetSchedules,
))
.await?;
let ScheduleServiceResponse::ServiceCall(service_call) = response else {
return Ok(None);
};
let ServiceCallResponse::GetSchedules(schedules) = service_call;
for schedule in schedules {
if schedule.state != ScheduleState::Active {
continue;
}
if let Some(schedule_next_time) = schedule.next_run_time {
match next_time {
Some(current_time) => {
if schedule_next_time < current_time {
next_time = Some(schedule_next_time);
}
}
None => next_time = Some(schedule_next_time),
}
}
}
if let Some(schedule_next_time) = next_time {
let now = Utc::now().naive_utc();
let duration = schedule_next_time.signed_duration_since(now);
Ok(Some(Duration::seconds(duration.num_seconds().max(0))))
} else {
Ok(None)
}
}
}

View File

@ -1,304 +0,0 @@
use crate::core::app_config::AppConfig;
use crate::core::database_manager::DatabaseManager;
use crate::core::event_bus::EventBus;
use crate::interface::repository::schedule::ScheduleRepository;
use crate::interface::service_unit::ServiceUnit;
use crate::model::backup::backup_schedule::*;
use crate::model::error::Error;
use crate::model::error::system::SystemError;
use crate::model::error::task::TaskError;
use crate::model::event::execution::*;
use crate::model::event::schedule::*;
use crate::model::log::system::SystemLog;
use async_trait::async_trait;
use chrono::{Duration, Months, Utc};
use macros::log;
use std::sync::Arc;
use tokio::select;
use tokio::sync::{mpsc, oneshot};
use tokio::time::sleep;
use tracing::error;
use uuid::Uuid;
pub struct ScheduleManager {
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
database_manager: Arc<DatabaseManager>,
}
impl ScheduleManager {
pub fn new(
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
database_manager: Arc<DatabaseManager>,
) -> Self {
ScheduleManager {
app_config,
event_bus,
database_manager,
}
}
pub async fn get_all_schedules(&self) -> Result<Vec<BackupSchedule>, Error> {
self.database_manager.get_all_backup_schedules().await
}
pub async fn create_schedule(&self, schedule: BackupSchedule) -> Result<(), Error> {
self.database_manager
.create_backup_schedule(&schedule)
.await
}
pub async fn modify_schedule(&self, schedule: BackupSchedule) -> Result<(), Error> {
self.database_manager
.modify_backup_schedule(&schedule)
.await
}
pub async fn remove_schedule(&self, uuid: Uuid) -> Result<(), Error> {
self.database_manager.remove_backup_schedule(uuid).await
}
pub async fn active_schedule(&self, uuid: Uuid) -> Result<(), Error> {
if let Some(mut schedule) = self.database_manager.get_backup_schedule(uuid).await? {
schedule.state = ScheduleState::Active;
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
}
Ok(())
}
pub async fn pause_schedule(&self, uuid: Uuid) -> Result<(), Error> {
if let Some(mut schedule) = self.database_manager.get_backup_schedule(uuid).await? {
schedule.state = ScheduleState::Paused;
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
}
Ok(())
}
pub async fn disable_schedule(&self, uuid: Uuid) -> Result<(), Error> {
if let Some(mut schedule) = self.database_manager.get_backup_schedule(uuid).await? {
schedule.state = ScheduleState::Disabled;
self.database_manager
.modify_backup_schedule(&schedule)
.await?;
}
Ok(())
}
pub async fn execute_ready_schedule(&self) -> Result<(), Error> {
let event_bus = self.event_bus.clone();
let database_manager = self.database_manager.clone();
let now = Utc::now().naive_utc();
let mut schedules = self.get_all_schedules().await?;
for schedule in schedules.iter_mut() {
if schedule.state != ScheduleState::Active {
continue;
}
if let Some(next_run_time) = schedule.next_run_time {
if next_run_time < now {
let execution = schedule.to_execution();
event_bus.publish(ExecutionAddRequest { execution });
self.update_next_run_time(schedule);
database_manager.modify_backup_schedule(&schedule).await?;
}
}
}
Ok(())
}
fn update_next_run_time(&self, schedule: &mut BackupSchedule) {
if schedule.next_run_time.is_none() {
return;
}
let now = Utc::now().naive_utc();
let old_next_run_time = schedule.next_run_time.unwrap();
let new_next_run_time = match schedule.interval {
ScheduleInterval::Once => None,
ScheduleInterval::Daily => Some(old_next_run_time + Duration::days(1)),
ScheduleInterval::Weekly => Some(old_next_run_time + Duration::days(7)),
ScheduleInterval::Monthly => Some(
old_next_run_time
.checked_add_months(Months::new(1))
.unwrap_or(old_next_run_time + Duration::days(30)),
),
};
schedule.last_run_time = Some(now);
schedule.next_run_time = new_next_run_time;
}
}
struct ScheduleTimer {
app_config: Arc<AppConfig>,
schedule_manager: Arc<ScheduleManager>,
shutdown_rx: Option<oneshot::Receiver<()>>,
refresh_rx: mpsc::UnboundedReceiver<()>,
}
impl ScheduleTimer {
pub fn new(
app_config: Arc<AppConfig>,
schedule_manager: Arc<ScheduleManager>,
shutdown_rx: oneshot::Receiver<()>,
refresh_rx: mpsc::UnboundedReceiver<()>,
) -> Self {
ScheduleTimer {
app_config,
schedule_manager,
shutdown_rx: Some(shutdown_rx),
refresh_rx,
}
}
pub async fn run(mut self) {
let schedule_manager = self.schedule_manager.clone();
match self.shutdown_rx.take() {
Some(mut shutdown_rx) => loop {
let mut sleep_time = match self.calculate_sleep_duration().await {
Ok(Some(duration)) => duration,
Ok(None) => Duration::seconds(self.app_config.default_wakeup_time),
Err(err) => {
error!("{}", err);
Duration::seconds(self.app_config.default_wakeup_time)
}
};
if sleep_time < Duration::seconds(0) {
sleep_time = Duration::seconds(0);
}
select! {
biased;
_ = &mut shutdown_rx => { break; }
_ = self.refresh_rx.recv() => {}
_ = sleep(sleep_time.to_std().unwrap()) => {}
}
if let Err(err) = schedule_manager.execute_ready_schedule().await {
error!("{}", err);
}
},
None => log!(TaskError::IllegalRunState),
}
}
async fn calculate_sleep_duration(&self) -> Result<Option<Duration>, Error> {
let mut next_time = None;
let schedules = self.schedule_manager.get_all_schedules().await?;
for schedule in schedules {
if schedule.state != ScheduleState::Active {
continue;
}
if let Some(schedule_next_time) = schedule.next_run_time {
match next_time {
Some(current_time) => {
if schedule_next_time < current_time {
next_time = Some(schedule_next_time);
}
}
None => next_time = Some(schedule_next_time),
}
}
}
if let Some(schedule_next_time) = next_time {
let now = Utc::now().naive_utc();
let duration = schedule_next_time.signed_duration_since(now);
Ok(Some(Duration::seconds(duration.num_seconds().max(0))))
} else {
Ok(None)
}
}
}
#[async_trait]
impl ServiceUnit for ScheduleManager {
async fn run_impl(self: Arc<Self>, mut shutdown_rx: oneshot::Receiver<()>) {
let (timer_shutdown_tx, timer_shutdown_rx) = oneshot::channel();
let (timer_refresh_tx, timer_refresh_rx) = mpsc::unbounded_channel();
let schedule_timer = ScheduleTimer::new(
self.app_config.clone(),
self.clone(),
timer_shutdown_rx,
timer_refresh_rx,
);
tokio::spawn(schedule_timer.run());
let event_bus = self.event_bus.clone();
let schedule_manager = self.clone();
let create_schedule = event_bus.subscribe::<ScheduleCreateRequest>();
let modify_schedule = event_bus.subscribe::<ScheduleModifyRequest>();
let remove_schedule = event_bus.subscribe::<ScheduleRemoveRequest>();
let active_schedule = event_bus.subscribe::<ScheduleActiveRequest>();
let pause_schedule = event_bus.subscribe::<SchedulePauseRequest>();
let disable_schedule = event_bus.subscribe::<ScheduleDisableRequest>();
let sleep_duration = Duration::milliseconds(self.app_config.internal_timestamp)
.to_std()
.unwrap();
loop {
if shutdown_rx.try_recv().is_ok() {
log!(SystemLog::Terminating);
if timer_shutdown_tx.send(()).is_err() {
log!(SystemError::TerminateError(
"Fail send shutdown signal to timer"
))
} else {
log!(SystemLog::TerminateComplete);
}
break;
}
let mut need_refresh = false;
while let Ok(event) = create_schedule.try_recv() {
match schedule_manager.create_schedule(event.schedule).await {
Ok(_) => need_refresh = true,
Err(err) => error!("{}", err),
}
}
while let Ok(event) = modify_schedule.try_recv() {
match schedule_manager.modify_schedule(event.schedule).await {
Ok(_) => need_refresh = true,
Err(err) => error!("{}", err),
}
}
while let Ok(event) = remove_schedule.try_recv() {
match schedule_manager.remove_schedule(event.schedule_id).await {
Ok(_) => need_refresh = true,
Err(err) => error!("{}", err),
}
}
while let Ok(event) = active_schedule.try_recv() {
match schedule_manager.active_schedule(event.schedule_id).await {
Ok(_) => need_refresh = true,
Err(err) => error!("{}", err),
}
}
while let Ok(event) = pause_schedule.try_recv() {
match schedule_manager.pause_schedule(event.schedule_id).await {
Ok(_) => need_refresh = true,
Err(err) => error!("{}", err),
}
}
while let Ok(event) = disable_schedule.try_recv() {
match schedule_manager.disable_schedule(event.schedule_id).await {
Ok(_) => need_refresh = true,
Err(err) => error!("{}", err),
}
}
if need_refresh {
let _ = timer_refresh_tx.send(());
}
sleep(sleep_duration).await;
}
}
}

View File

@ -1,99 +1,66 @@
use crate::core::app_config::AppConfig;
use crate::core::backup_engine::BackupEngine;
use crate::core::database_manager::DatabaseManager;
use crate::core::event_bus::EventBus;
use crate::core::gui_manager::GuiManager;
use crate::core::io_manager::IOManager;
use crate::core::progress_tracker::ProgressTracker;
use crate::core::schedule_manager::ScheduleManager;
use crate::interface::service_unit::ServiceUnit;
use crate::core::backup::backup_service::BackupService;
use crate::core::gui::gui_manager::GuiManager;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::database_manager::DatabaseManager;
use crate::core::infrastructure::io_manager::IOManager;
use crate::core::schedule::schedule_service::ScheduleService;
use crate::model::error::Error;
use crate::model::error::system::SystemError;
use crate::model::log::system::SystemLog;
#[cfg(any(target_os = "windows", not(debug_assertions)))]
use crate::platform::elevate;
use crate::utils::database_lock::DatabaseLock;
use crate::utils::logging::Logging;
use macros::log;
#[cfg(not(debug_assertions))]
use privilege::user::privileged;
use std::mem;
#[cfg(not(debug_assertions))]
use std::process;
use std::sync::Arc;
use tokio::sync::oneshot;
pub struct System {
pub io_manager: Arc<IOManager>,
pub database_manager: Arc<DatabaseManager>,
pub backup_engine: Arc<BackupEngine>,
pub schedule_manager: Arc<ScheduleManager>,
pub gui_manager: Arc<GuiManager>,
pub _database_lock: DatabaseLock,
pub shutdowns: Vec<oneshot::Sender<()>>,
app_config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
database_manager: Arc<DatabaseManager>,
actor_system: Arc<ActorSystem>,
}
impl System {
pub async fn new() -> Result<Self, Error> {
let app_config = Arc::new(AppConfig::new()?);
let io_manager = Arc::new(IOManager::new(app_config.clone()));
let database_manager = Arc::new(DatabaseManager::new().await?);
let actor_system = Arc::new(ActorSystem::new());
let system = Self {
app_config,
io_manager,
database_manager,
actor_system,
};
Ok(system)
}
pub async fn run(&self) -> Result<(), Error> {
Logging::initialize().await;
log!(SystemLog::Initializing);
Self::elevate_privileges()?;
let app_config = Arc::new(AppConfig::new()?);
let event_bus = Arc::new(EventBus::new());
let io_manager = Arc::new(IOManager::new(app_config.clone()));
let _database_lock = DatabaseLock::acquire().await?;
let database_manager = Arc::new(DatabaseManager::new().await?);
let progress_tracker = Arc::new(ProgressTracker::new(io_manager.clone()));
let backup_engine = Arc::new(BackupEngine::new(
let app_config = self.app_config.clone();
let io_manager = self.io_manager.clone();
let database_manager = self.database_manager.clone();
let actor_system = self.actor_system.clone();
BackupService::init(app_config.clone(), io_manager.clone(), actor_system.clone()).await;
ScheduleService::init(
app_config.clone(),
event_bus.clone(),
io_manager.clone(),
progress_tracker.clone(),
));
let schedule_manager = Arc::new(ScheduleManager::new(
app_config.clone(),
event_bus.clone(),
database_manager.clone(),
));
let gui_manager = Arc::new(GuiManager::new(
app_config.clone(),
event_bus.clone(),
backup_engine.clone(),
schedule_manager.clone(),
));
actor_system.clone(),
)
.await?;
let gui_manager = Arc::new(GuiManager::new(app_config.clone(), actor_system.clone()));
log!(SystemLog::InitializeComplete);
Ok(Self {
io_manager,
database_manager,
backup_engine,
schedule_manager,
gui_manager,
_database_lock,
shutdowns: Vec::new(),
})
gui_manager.start().await
}
pub async fn run(&mut self) -> Result<(), Error> {
let gui_manager = self.gui_manager.clone();
let backup_engine_shutdown = self.backup_engine.clone().run().await;
let schedule_manager_shutdown = self.schedule_manager.clone().run().await;
self.shutdowns.push(backup_engine_shutdown);
self.shutdowns.push(schedule_manager_shutdown);
gui_manager.start()
}
pub async fn terminate(&mut self) {
log!(SystemLog::Terminating);
let shutdowns = mem::take(&mut self.shutdowns);
for shutdown in shutdowns {
if shutdown.send(()).is_err() {
log!(SystemError::ShutdownSignalFailed);
}
}
self.backup_engine.stop_all_executions().await;
self.database_manager.close_connection().await;
self.io_manager.terminate();
log!(SystemLog::TerminateComplete);
pub fn shutdown(&self) {
self.actor_system.shutdown();
}
fn elevate_privileges() -> Result<(), Error> {

View File

@ -0,0 +1,14 @@
use crate::interface::actor::message::Message;
use crate::model::error::Error;
use async_trait::async_trait;
#[async_trait]
pub trait Actor: Send + 'static {
type Message: Message;
async fn pre_start(&mut self);
async fn post_stop(&mut self);
async fn receive(
&mut self,
message: Self::Message,
) -> Result<<Self::Message as Message>::Response, Error>;
}

View File

@ -0,0 +1,3 @@
pub trait Message: Send + 'static {
type Response: Send + 'static;
}

View File

@ -0,0 +1,2 @@
pub mod actor;
pub mod message;

View File

@ -1 +0,0 @@
pub trait Event: Clone + Send + 'static {}

View File

@ -1,12 +1,11 @@
use crate::model::error::io::IOError;
use crate::model::error::system::SystemError;
use crate::model::error::Error;
use crate::model::backup::backup_execution::HashType;
use crate::model::core::backup::backup_execution::HashType;
use crate::platform::attributes::*;
use crate::utils::file_hash::*;
use crate::utils::file_lock::FileLock;
use async_trait::async_trait;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tokio::sync::Semaphore;
@ -20,7 +19,7 @@ pub trait FileSystemTrait {
fn semaphore(&self) -> Arc<Semaphore>;
async fn is_symlink(&self, path: &PathBuf) -> Result<bool, Error> {
async fn is_symlink(&self, path: &Path) -> Result<bool, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -29,18 +28,18 @@ pub trait FileSystemTrait {
let symlink_metadata = tokio::fs::symlink_metadata(path)
.await
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
Ok(symlink_metadata.file_type().is_symlink())
}
async fn copy_symlink(
&self,
source_link: &PathBuf,
destination_link: &PathBuf,
source_link: &Path,
destination_link: &Path,
) -> Result<(), Error>;
async fn list_directory(&self, path: &PathBuf) -> Result<Vec<PathBuf>, Error> {
async fn list_directory(&self, path: &Path) -> Result<Vec<PathBuf>, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -50,18 +49,18 @@ pub trait FileSystemTrait {
let mut result = Vec::new();
let reader = fs::read_dir(path)
.await
.map_err(|err| IOError::ReadDirectoryFailed(path.clone(), err))?;
.map_err(|err| IOError::ReadDirectoryFailed(path, err))?;
let mut entries = ReadDirStream::new(reader);
while let Some(entry) = entries.next().await {
let path = entry
.map_err(|err| IOError::ReadDirectoryFailed(path.clone(), err))?
.map_err(|err| IOError::ReadDirectoryFailed(path, err))?
.path();
result.push(path);
}
Ok(result)
}
async fn create_directory(&self, path: &PathBuf) -> Result<(), Error> {
async fn create_directory(&self, path: &Path) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -70,11 +69,11 @@ pub trait FileSystemTrait {
fs::create_dir_all(path)
.await
.map_err(|err| IOError::CreateDirectoryFailed(path.clone(), err))?;
.map_err(|err| IOError::CreateDirectoryFailed(path, err))?;
Ok(())
}
async fn delete_directory(&self, path: &PathBuf) -> Result<(), Error> {
async fn delete_directory(&self, path: &Path) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -83,11 +82,11 @@ pub trait FileSystemTrait {
fs::remove_dir_all(path)
.await
.map_err(|err| IOError::DeleteDirectoryFailed(path.clone(), err))?;
.map_err(|err| IOError::DeleteDirectoryFailed(path, err))?;
Ok(())
}
async fn copy_file(&self, source: &PathBuf, destination: &PathBuf) -> Result<(), Error> {
async fn copy_file(&self, source: &Path, destination: &Path) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -96,11 +95,11 @@ pub trait FileSystemTrait {
fs::copy(source, destination)
.await
.map_err(|err| IOError::CopyFileFailed(source.clone(), destination.clone(), err))?;
.map_err(|err| IOError::CopyFileFailed(source, destination, err))?;
Ok(())
}
async fn delete_file(&self, path: &PathBuf) -> Result<(), Error> {
async fn delete_file(&self, path: &Path) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -109,15 +108,15 @@ pub trait FileSystemTrait {
fs::remove_file(path)
.await
.map_err(|err| IOError::DeleteFileFailed(path.clone(), err))?;
.map_err(|err| IOError::DeleteFileFailed(path, err))?;
Ok(())
}
async fn get_attributes(&self, path: &PathBuf) -> Result<Attributes, Error>;
async fn get_attributes(&self, path: &Path) -> Result<Attributes, Error>;
async fn set_attributes(&self, path: &PathBuf, attributes: Attributes) -> Result<(), Error>;
async fn set_attributes(&self, path: &Path, attributes: Attributes) -> Result<(), Error>;
async fn copy_attributes(&self, source: &PathBuf, destination: &PathBuf) -> Result<(), Error> {
async fn copy_attributes(&self, source: &Path, destination: &Path) -> Result<(), Error> {
let source_attributes = self.get_attributes(source).await?;
self.set_attributes(destination, source_attributes).await?;
Ok(())
@ -125,46 +124,33 @@ pub trait FileSystemTrait {
async fn compare_attributes(
&self,
source: &PathBuf,
destination: &PathBuf,
source: &Path,
destination: &Path,
) -> Result<bool, Error> {
let source_attributes = self.get_attributes(source).await?;
let destination_attributes = self.get_attributes(destination).await?;
Ok(source_attributes == destination_attributes)
}
async fn get_permission(&self, path: &PathBuf) -> Result<Permissions, Error>;
async fn get_permission(&self, path: &Path) -> Result<Permissions, Error>;
async fn set_permission(&self, path: &PathBuf, permissions: Permissions) -> Result<(), Error>;
async fn set_permission(&self, path: &Path, permissions: Permissions) -> Result<(), Error>;
async fn copy_permission(&self, source: &PathBuf, destination: &PathBuf) -> Result<(), Error> {
async fn copy_permission(&self, source: &Path, destination: &Path) -> Result<(), Error> {
let source_permissions = self.get_permission(source).await?;
self.set_permission(destination, source_permissions).await?;
Ok(())
}
async fn acquire_file_lock(&self, path: &PathBuf) -> Result<FileLock, Error> {
async fn calculate_hash(&self, path: &Path, hash_type: HashType) -> Result<Vec<u8>, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let file_lock = FileLock::new(path).await?;
Ok(file_lock)
}
async fn calculate_hash(&self, path: &PathBuf, hash_type: HashType) -> Result<Vec<u8>, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let path_clone = path.clone();
let path = path.to_path_buf();
let hash = spawn_blocking(move || {
let path = path_clone;
match hash_type {
HashType::MD5 => md5(path),
HashType::SHA3 => sha3(path),
@ -181,8 +167,8 @@ pub trait FileSystemTrait {
async fn standard_compare(
&self,
source: &PathBuf,
destination: &PathBuf,
source: &Path,
destination: &Path,
) -> Result<bool, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
@ -193,11 +179,11 @@ pub trait FileSystemTrait {
let source_metadata =
fs::metadata(source)
.await
.map_err(|err| IOError::GetMetadataFailed(source.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(source, err))?;
let destination_metadata =
fs::metadata(destination)
.await
.map_err(|err| IOError::GetMetadataFailed(destination.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(destination, err))?;
if source_metadata.len() != destination_metadata.len() {
return Ok(false);
@ -205,11 +191,11 @@ pub trait FileSystemTrait {
let source_modified =
source_metadata
.modified()
.map_err(|err| IOError::GetMetadataFailed(source.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(source, err))?;
let destination_modified =
destination_metadata
.modified()
.map_err(|err| IOError::GetMetadataFailed(destination.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(destination, err))?;
if source_modified != destination_modified {
return Ok(false);
}
@ -218,8 +204,8 @@ pub trait FileSystemTrait {
async fn advance_compare(
&self,
source: &PathBuf,
destination: &PathBuf,
source: &Path,
destination: &Path,
) -> Result<bool, Error> {
if !self.standard_compare(source, destination).await? {
return Ok(false);
@ -234,8 +220,8 @@ pub trait FileSystemTrait {
async fn thorough_compare(
&self,
source: &PathBuf,
destination: &PathBuf,
source: &Path,
destination: &Path,
hash_type: HashType,
) -> Result<bool, Error> {
if !self.advance_compare(source, destination).await? {

View File

@ -1,4 +1,3 @@
pub mod event;
pub mod actor;
pub mod file_system;
pub mod repository;
pub mod service_unit;

View File

@ -1,5 +1,5 @@
use crate::core::database_manager::DatabaseManager;
use crate::model::backup::backup_schedule::BackupSchedule;
use crate::core::infrastructure::database_manager::DatabaseManager;
use crate::model::core::schedule::backup_schedule::BackupSchedule;
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
use crate::model::error::misc::MiscError;

View File

@ -1,16 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::oneshot;
#[async_trait]
pub trait ServiceUnit: 'static {
async fn run(self: Arc<Self>) -> oneshot::Sender<()> {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
tokio::spawn(self.run_impl(shutdown_rx));
shutdown_tx
}
async fn run_impl(self: Arc<Self>, shutdown_rx: oneshot::Receiver<()>);
}

View File

@ -11,8 +11,8 @@ mod utils;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut system = System::new().await?;
let system = System::new().await?;
system.run().await?;
system.terminate().await;
system.shutdown();
Ok(())
}

View File

@ -1,3 +0,0 @@
pub mod backup_execution;
pub mod backup_schedule;
pub mod progress_data;

View File

@ -0,0 +1,44 @@
use crate::interface::actor::message::Message;
use crate::model::core::actor::envelope::Envelope;
use crate::model::error::actor::ActorError;
use crate::model::error::Error;
use tokio::sync::{mpsc, oneshot};
pub struct ActorRef<M: Message> {
tx: mpsc::UnboundedSender<Envelope<M>>,
}
impl<M: Message> ActorRef<M> {
pub fn new(tx: mpsc::UnboundedSender<Envelope<M>>) -> Self {
Self { tx }
}
pub async fn tell(&self, message: M) -> Result<(), Error> {
let envelope = Envelope::Tell(message);
self.tx
.send(envelope)
.map_err(|_| ActorError::SendMessageError)?;
Ok(())
}
pub async fn ask(&self, message: M) -> Result<M::Response, Error> {
let (reply_tx, reply_rx) = oneshot::channel::<M::Response>();
let envelope = Envelope::Ask {
message,
reply_to: reply_tx,
};
self.tx
.send(envelope)
.map_err(|_| ActorError::SendMessageError)?;
let reply = reply_rx.await.map_err(|_| ActorError::ActorNotResponding)?;
Ok(reply)
}
}
impl<M: Message> Clone for ActorRef<M> {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
}
}
}

View File

@ -0,0 +1,59 @@
use crate::interface::actor::actor::Actor;
use crate::model::core::actor::actor_ref::ActorRef;
use crate::model::core::actor::envelope::Envelope;
use crate::model::error::actor::ActorError;
use macros::log;
use tokio::select;
use tokio::sync::{mpsc, oneshot};
pub struct ActorRuntime<A: Actor> {
actor: A,
rx: mpsc::UnboundedReceiver<Envelope<A::Message>>,
}
impl<A: Actor> ActorRuntime<A> {
pub fn new(actor: A) -> (Self, ActorRef<A::Message>) {
let (tx, rx) = mpsc::unbounded_channel();
let actor_ref = ActorRef::new(tx);
let runtime = Self { actor, rx };
(runtime, actor_ref)
}
pub async fn run(mut self) -> oneshot::Sender<()> {
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
tokio::spawn(async move {
self.actor.pre_start().await;
loop {
select! {
envelope = self.rx.recv() => {
match envelope {
Some(Envelope::Tell(message)) => {
if self.actor.receive(message).await.is_err() {
log!(ActorError::SendMessageError);
}
}
Some(Envelope::Ask { message, reply_to }) => {
match self.actor.receive(message).await {
Ok(response) => {
if reply_to.send(response).is_err() {
log!(ActorError::SendMessageError);
}
}
Err(_) => {
log!(ActorError::SendMessageError);
}
}
}
None => break,
}
}
_ = &mut shutdown_rx => {
break;
}
}
}
self.actor.post_stop().await;
});
shutdown_tx
}
}

View File

@ -0,0 +1,10 @@
use crate::interface::actor::message::Message;
use tokio::sync::oneshot;
pub enum Envelope<M: Message> {
Tell(M),
Ask {
message: M,
reply_to: oneshot::Sender<M::Response>,
}
}

View File

@ -0,0 +1,3 @@
pub mod actor_ref;
pub mod actor_runtime;
pub mod envelope;

View File

@ -40,8 +40,7 @@ pub enum ComparisonMode {
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct BackupOptions {
pub mirror: bool,
pub lock_source: bool,
pub mirror: bool,
pub backup_permission: bool,
pub follow_symlinks: bool,
}

View File

@ -0,0 +1,29 @@
use crate::interface::actor::message::Message;
use crate::model::core::backup::backup_execution::BackupExecution;
use uuid::Uuid;
pub enum BackupServiceMessage {
ServiceCall(ServiceCallMessage),
}
pub enum BackupServiceResponse {
ServiceCall(ServiceCallResponse),
None,
}
impl Message for BackupServiceMessage {
type Response = BackupServiceResponse;
}
pub enum ServiceCallMessage {
AddExecution(BackupExecution),
RemoveExecution(Uuid),
StartExecution(Uuid),
SuspendExecution(Uuid),
ResumeExecution(Uuid),
GetExecutions,
}
pub enum ServiceCallResponse {
GetExecutions(Vec<(Uuid, BackupExecution)>)
}

View File

@ -0,0 +1,3 @@
pub mod backup_execution;
pub mod message;
pub mod progress_data;

View File

@ -0,0 +1,25 @@
use crate::interface::actor::message::Message;
use crate::model::error::Error;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone)]
pub enum GuiMessage {
FolderProcess {
uuid: Uuid,
folder: PathBuf,
},
ExecutionProgress {
uuid: Uuid,
processed_files: usize,
error_count: usize,
},
ExecutionErrors {
uuid: Uuid,
errors: Vec<Error>,
},
}
impl Message for GuiMessage {
type Response = ();
}

View File

@ -0,0 +1 @@
pub mod message;

4
src/model/core/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod actor;
pub mod backup;
pub mod gui;
pub mod schedule;

View File

@ -1,4 +1,4 @@
use crate::model::backup::backup_execution::*;
use crate::model::core::backup::backup_execution::*;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

View File

@ -0,0 +1,35 @@
use uuid::Uuid;
use crate::interface::actor::message::Message;
use crate::model::core::schedule::backup_schedule::BackupSchedule;
pub enum ScheduleServiceMessage {
UnitNotification(UnitNotificationMessage),
ServiceCall(ServiceCallMessage),
}
pub enum ScheduleServiceResponse {
ServiceCall(ServiceCallResponse),
None,
}
pub enum UnitNotificationMessage {
CheckSchedule,
}
pub enum ServiceCallMessage {
AddSchedule(BackupSchedule),
ModifySchedule(BackupSchedule),
RemoveSchedule(Uuid),
ActivateSchedule(Uuid),
PauseSchedule(Uuid),
DisableSchedule(Uuid),
GetSchedules,
}
pub enum ServiceCallResponse {
GetSchedules(Vec<BackupSchedule>),
}
impl Message for ScheduleServiceMessage {
type Response = ScheduleServiceResponse;
}

View File

@ -0,0 +1,2 @@
pub mod backup_schedule;
pub mod message;

15
src/model/error/actor.rs Normal file
View File

@ -0,0 +1,15 @@
use macros::traceable;
traceable! {
ActorError {
#[no_source]
#[error("Actor not found")]
ActorNotFound => tracing::Level::ERROR,
#[no_source]
#[error("Actor not responding")]
ActorNotResponding => tracing::Level::WARN,
#[no_source]
#[error("Failed to send message to actor")]
SendMessageError => tracing::Level::ERROR,
}
}

View File

@ -1,8 +0,0 @@
use macros::traceable;
traceable! {
EventError {
#[error("Placeholder")]
Placeholder => tracing::Level::INFO,
}
}

View File

@ -1,24 +1,24 @@
pub mod actor;
pub mod database;
pub mod event;
pub mod io;
pub mod misc;
pub mod system;
pub mod task;
use crate::model::error::actor::ActorError;
use crate::model::error::database::DatabaseError;
use crate::model::error::event::EventError;
use crate::model::error::io::IOError;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError;
use crate::model::error::task::TaskError;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
pub enum Error {
#[error("{0}")]
Database(DatabaseError),
Actor(ActorError),
#[error("{0}")]
Event(EventError),
Database(DatabaseError),
#[error("{0}")]
IO(IOError),
#[error("{0}")]
@ -29,15 +29,15 @@ pub enum Error {
Task(TaskError),
}
impl From<DatabaseError> for Error {
fn from(error: DatabaseError) -> Self {
Self::Database(error)
impl From<ActorError> for Error {
fn from(error: ActorError) -> Self {
Self::Actor(error)
}
}
impl From<EventError> for Error {
fn from(error: EventError) -> Self {
Self::Event(error)
impl From<DatabaseError> for Error {
fn from(error: DatabaseError) -> Self {
Self::Database(error)
}
}

View File

@ -9,26 +9,5 @@ traceable! {
#[no_source]
#[error("Task not found")]
ExecutionNotFound => tracing::Level::ERROR,
#[error("Failed to stop task")]
StopExecutionFailed => tracing::Level::ERROR,
#[error("Failed to load schedule")]
LoadScheduleFailed => tracing::Level::ERROR,
#[error("Failed to enable schedule")]
EnableScheduleFailed => tracing::Level::ERROR,
#[error("Failed to pause schedule")]
PauseScheduleFailed => tracing::Level::ERROR,
#[error("Failed to disable schedule")]
DisableScheduleFailed => tracing::Level::ERROR,
#[error("Failed to resume schedule")]
ResumeScheduleFailed => tracing::Level::ERROR,
#[error("Failed to remove schedule")]
RemoveScheduleFailed => tracing::Level::ERROR,
}
}

View File

@ -1,12 +0,0 @@
use crate::interface::event::Event;
use crate::model::error::Error;
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct BackupError {
pub task_id: Uuid,
pub error: Error,
}
impl Event for BackupError {}
//todo Need add global error event

View File

@ -1,41 +0,0 @@
use crate::interface::event::Event;
use crate::model::backup::backup_execution::BackupExecution;
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct ExecutionAddRequest {
pub execution: BackupExecution,
}
impl Event for ExecutionAddRequest {}
#[derive(Clone, Debug)]
pub struct ExecutionRemoveRequest {
pub execution_id: Uuid,
}
impl Event for ExecutionRemoveRequest {}
#[derive(Clone, Debug)]
pub struct ExecutionStartRequest {
pub execution_id: Uuid,
}
impl Event for ExecutionStartRequest {}
#[derive(Clone, Debug)]
pub struct ExecutionSuspendRequest {
pub execution_id: Uuid,
}
impl Event for ExecutionSuspendRequest {}
#[derive(Clone, Debug)]
pub struct ExecutionResumeRequested {
pub execution_id: Uuid,
}
impl Event for ExecutionResumeRequested {}
#[derive(Clone, Debug)]
pub struct ExecutionProgress {
pub task_id: Uuid,
pub processed_files: usize,
pub error_count: usize,
}
impl Event for ExecutionProgress {}

View File

@ -1,10 +0,0 @@
use crate::interface::event::Event;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct FolderProcessing {
pub execution_id: Uuid,
pub current_folder: PathBuf,
}
impl Event for FolderProcessing {}

View File

@ -1,4 +0,0 @@
pub mod error;
pub mod execution;
pub mod filesystem;
pub mod schedule;

View File

@ -1,39 +0,0 @@
use uuid::Uuid;
use crate::interface::event::Event;
use crate::model::backup::backup_schedule::BackupSchedule;
#[derive(Clone, Debug)]
pub struct ScheduleCreateRequest {
pub schedule: BackupSchedule,
}
impl Event for ScheduleCreateRequest {}
#[derive(Clone, Debug)]
pub struct ScheduleModifyRequest {
pub schedule: BackupSchedule,
}
impl Event for ScheduleModifyRequest {}
#[derive(Clone, Debug)]
pub struct ScheduleRemoveRequest {
pub schedule_id: Uuid,
}
impl Event for ScheduleRemoveRequest {}
#[derive(Clone, Debug)]
pub struct ScheduleActiveRequest {
pub schedule_id: Uuid,
}
impl Event for ScheduleActiveRequest {}
#[derive(Clone, Debug)]
pub struct SchedulePauseRequest {
pub schedule_id: Uuid,
}
impl Event for SchedulePauseRequest {}
#[derive(Clone, Debug)]
pub struct ScheduleDisableRequest {
pub schedule_id: Uuid,
}
impl Event for ScheduleDisableRequest {}

View File

@ -1,5 +1,4 @@
pub mod backup;
pub mod core;
pub mod config;
pub mod error;
pub mod event;
pub mod log;

View File

@ -1,7 +1,7 @@
use libc::{gid_t, mode_t, uid_t};
use libc::{gid_t, uid_t};
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Eq)]
pub struct Attributes {
pub attributes: u32,
pub creation_time: SystemTime,
@ -9,6 +9,14 @@ pub struct Attributes {
pub change_time: SystemTime,
}
impl PartialEq for Attributes {
fn eq(&self, other: &Self) -> bool {
self.attributes == other.attributes
&& self.creation_time == other.creation_time
&& self.change_time == other.change_time
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Permissions {
pub uid: uid_t,
@ -18,28 +26,3 @@ pub struct Permissions {
pub is_setuid: bool,
pub is_setgid: bool,
}
impl Permissions {
pub fn new(uid: uid_t, gid: gid_t, mode: mode_t) -> Self {
Self {
uid,
gid,
mode: mode as u32,
is_sticky: (mode & libc::S_ISVTX as mode_t) != 0,
is_setuid: (mode & libc::S_ISUID as mode_t) != 0,
is_setgid: (mode & libc::S_ISGID as mode_t) != 0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExtendedAttribute {
pub name: String,
pub value: Vec<u8>,
}
impl ExtendedAttribute {
pub fn new(name: String, value: Vec<u8>) -> Self {
Self { name, value }
}
}

View File

@ -1,4 +1,4 @@
pub const DATABASE_PATH: &'static str = "./mirrorSphere.db";
pub const DATABASE_URL: &'static str = "sqlite://./mirrorSphere.db";
pub const DATABASE_LOCK_PATH: &'static str = "./db.lock";
pub const PROGRESS_SAVE_PATH: &'static str = "./progress";
pub const DATABASE_PATH: &str = "./mirrorSphere.db";
pub const DATABASE_URL: &str = "sqlite://./mirrorSphere.db";
pub const DATABASE_LOCK_PATH: &str = "./db.lock";
pub const PROGRESS_SAVE_PATH: &str = "./progress";

View File

@ -1,8 +1,9 @@
use crate::model::error::Error;
use crate::model::error::system::SystemError;
use std::process::Command;
use std::{env, io};
use std::env;
#[allow(dead_code)]
pub fn elevate() -> Result<(), Error> {
let exe = env::current_exe()
.map_err(|_| SystemError::RunAsAdminFailed)?;

View File

@ -7,7 +7,7 @@ use async_trait::async_trait;
use libc::mode_t;
use std::ffi::CString;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::Semaphore;
@ -27,27 +27,21 @@ impl FileSystemTrait for FileSystem {
self.semaphore.clone()
}
async fn copy_symlink(
&self,
source_link: &PathBuf,
destination_link: &PathBuf,
) -> Result<(), Error> {
async fn copy_symlink(&self, source_link: &Path, destination_link: &Path) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
tokio::fs::symlink(&source_link, destination_link)
tokio::fs::symlink(source_link, destination_link)
.await
.map_err(|err| {
IOError::CreateSymbolLinkFailed(source_link.clone(), destination_link.clone(), err)
})?;
.map_err(|err| IOError::CreateSymbolLinkFailed(source_link, destination_link, err))?;
Ok(())
}
async fn get_attributes(&self, path: &PathBuf) -> Result<Attributes, Error> {
async fn get_attributes(&self, path: &Path) -> Result<Attributes, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -56,7 +50,7 @@ impl FileSystemTrait for FileSystem {
let metadata = tokio::fs::metadata(path)
.await
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let mode = metadata.mode();
let file_type = metadata.file_type();
@ -78,10 +72,10 @@ impl FileSystemTrait for FileSystem {
.unwrap_or_else(|_| metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH));
let last_access_time = metadata
.accessed()
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let change_time = metadata
.modified()
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let attributes = Attributes {
attributes,
@ -93,14 +87,14 @@ impl FileSystemTrait for FileSystem {
Ok(attributes)
}
async fn set_attributes(&self, path: &PathBuf, attributes: Attributes) -> Result<(), Error> {
async fn set_attributes(&self, path: &Path, attributes: Attributes) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let path_clone = path.clone();
let path_clone = path.to_path_buf();
let mode = attributes.attributes & 0o7777;
spawn_blocking(move || {
@ -127,14 +121,14 @@ impl FileSystemTrait for FileSystem {
Ok(())
}
async fn get_permission(&self, path: &PathBuf) -> Result<Permissions, Error> {
async fn get_permission(&self, path: &Path) -> Result<Permissions, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let path_clone = path.clone();
let path_clone = path.to_path_buf();
let permission = spawn_blocking(move || {
let path = path_clone;
@ -149,9 +143,9 @@ impl FileSystemTrait for FileSystem {
uid,
gid,
mode,
is_sticky: (mode & libc::S_ISVTX as u32) != 0,
is_setuid: (mode & libc::S_ISUID as u32) != 0,
is_setgid: (mode & libc::S_ISGID as u32) != 0,
is_sticky: (mode & libc::S_ISVTX) != 0,
is_setuid: (mode & libc::S_ISUID) != 0,
is_setgid: (mode & libc::S_ISGID) != 0,
})
})
.await
@ -160,14 +154,14 @@ impl FileSystemTrait for FileSystem {
Ok(permission)
}
async fn set_permission(&self, path: &PathBuf, permissions: Permissions) -> Result<(), Error> {
async fn set_permission(&self, path: &Path, permissions: Permissions) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let path_clone = path.clone();
let path_clone = path.to_path_buf();
spawn_blocking(move || {
let path = path_clone;
@ -176,26 +170,22 @@ impl FileSystemTrait for FileSystem {
unsafe {
if libc::chown(c_path.as_ptr(), permissions.uid, permissions.gid) != 0 {
return Err(
IOError::SetMetadataFailed(path.clone(), "libc chmod failed").into(),
);
return Err(IOError::SetMetadataFailed(path, "libc chmod failed").into());
}
let mut mode = permissions.mode & 0o7777;
if permissions.is_sticky {
mode |= libc::S_ISVTX as u32;
mode |= libc::S_ISVTX;
}
if permissions.is_setuid {
mode |= libc::S_ISUID as u32;
mode |= libc::S_ISUID;
}
if permissions.is_setgid {
mode |= libc::S_ISGID as u32;
mode |= libc::S_ISGID;
}
if libc::chmod(c_path.as_ptr(), mode as mode_t) != 0 {
return Err(
IOError::SetMetadataFailed(path.clone(), "libc chmod failed").into(),
);
return Err(IOError::SetMetadataFailed(path, "libc chmod failed").into());
}
}
@ -209,9 +199,9 @@ impl FileSystemTrait for FileSystem {
}
impl FileSystem {
fn set_file_times(path: &PathBuf, attributes: &Attributes) -> Result<(), Error> {
fn set_file_times(path: &Path, attributes: &Attributes) -> Result<(), Error> {
let c_path = CString::new(path.to_string_lossy().as_bytes())
.map_err(|err| IOError::SetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::SetMetadataFailed(path, err))?;
let access_time = Self::system_time_to_timespec(attributes.last_access_time)?;
let modify_time = Self::system_time_to_timespec(attributes.change_time)?;
@ -220,10 +210,7 @@ impl FileSystem {
unsafe {
if libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) != 0 {
Err(IOError::SetMetadataFailed(
path.clone(),
"libc utimensat failed",
))?;
Err(IOError::SetMetadataFailed(path, "libc utimensat failed"))?;
}
}

View File

@ -13,6 +13,8 @@ pub struct Attributes {
impl PartialEq for Attributes {
fn eq(&self, other: &Self) -> bool {
self.attributes == other.attributes
&& self.creation_time == other.creation_time
&& self.change_time == other.change_time
}
}

View File

@ -85,7 +85,7 @@ pub fn adjust_token_privileges() -> Result<(), Error> {
LookupPrivilegeValueW(PCWSTR::null(), privilege_name, &mut luid)
.map_err(SystemError::AdjustTokenPrivilegesFailed)?;
let mut token_privilege = TOKEN_PRIVILEGES {
let token_privilege = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
@ -96,7 +96,7 @@ pub fn adjust_token_privileges() -> Result<(), Error> {
AdjustTokenPrivileges(
token_handle,
false,
Some(&mut token_privilege),
Some(&token_privilege),
0,
None,
None,
@ -108,7 +108,7 @@ pub fn adjust_token_privileges() -> Result<(), Error> {
let last_error = GetLastError();
if last_error == ERROR_NOT_ALL_ASSIGNED {
Err(SystemError::AdjustTokenPrivilegesFailed(format!("{:?}", last_error)))?
Err(SystemError::AdjustTokenPrivilegesFailed(format!("{last_error:?}")))?
}
Ok(())

View File

@ -9,7 +9,7 @@ use async_trait::async_trait;
use chrono::{DateTime, Datelike, Timelike};
use std::os::windows::ffi::OsStrExt;
use std::os::windows::fs::MetadataExt;
use std::path::PathBuf;
use std::path::Path;
use std::ptr;
use std::sync::Arc;
use std::time::SystemTime;
@ -20,7 +20,7 @@ use windows::Win32::Security::Authorization::{
GetNamedSecurityInfoW, SE_FILE_OBJECT, SetNamedSecurityInfoW,
};
use windows::Win32::Security::{
ACL, BACKUP_SECURITY_INFORMATION, DACL_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION,
ACL, DACL_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION,
OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SACL_SECURITY_INFORMATION,
};
use windows::Win32::Storage::FileSystem::{
@ -44,11 +44,7 @@ impl FileSystemTrait for FileSystem {
self.semaphore.clone()
}
async fn copy_symlink(
&self,
source_link: &PathBuf,
destination_link: &PathBuf,
) -> Result<(), Error> {
async fn copy_symlink(&self, source_link: &Path, destination_link: &Path) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -57,7 +53,7 @@ impl FileSystemTrait for FileSystem {
let link_target = tokio::fs::read_link(source_link)
.await
.map_err(|err| IOError::ReadSymbolLinkFailed(source_link.clone(), err))?;
.map_err(|err| IOError::ReadSymbolLinkFailed(source_link, err))?;
if link_target.is_dir() {
tokio::fs::symlink_dir(&link_target, destination_link).await
@ -69,7 +65,7 @@ impl FileSystemTrait for FileSystem {
Ok(())
}
async fn get_attributes(&self, path: &PathBuf) -> Result<Attributes, Error> {
async fn get_attributes(&self, path: &Path) -> Result<Attributes, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -78,19 +74,19 @@ impl FileSystemTrait for FileSystem {
let metadata = tokio::fs::metadata(path)
.await
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let attributes = metadata.file_attributes();
let creation_time = metadata
.created()
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let last_access_time = metadata
.accessed()
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let change_time = metadata
.modified()
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
.map_err(|err| IOError::GetMetadataFailed(path, err))?;
let attributes = Attributes {
attributes,
@ -102,7 +98,7 @@ impl FileSystemTrait for FileSystem {
Ok(attributes)
}
async fn set_attributes(&self, path: &PathBuf, attributes: Attributes) -> Result<(), Error> {
async fn set_attributes(&self, path: &Path, attributes: Attributes) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
@ -113,7 +109,7 @@ impl FileSystemTrait for FileSystem {
let file_attributes = attributes.attributes;
let path = path.clone();
let path = path.to_path_buf();
spawn_blocking(move || unsafe {
SetFileAttributesW(
PCWSTR(file_path_wild.as_ptr()),
@ -155,10 +151,16 @@ impl FileSystemTrait for FileSystem {
Ok(())
}
async fn get_permission(&self, path: &PathBuf) -> Result<Permissions, Error> {
async fn get_permission(&self, path: &Path) -> Result<Permissions, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let file_path_wild: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let path = path.clone();
let path = path.to_path_buf();
let permission = spawn_blocking(move || unsafe {
let security_info = OWNER_SECURITY_INFORMATION
| GROUP_SECURITY_INFORMATION
@ -182,7 +184,10 @@ impl FileSystemTrait for FileSystem {
);
if result.is_err() {
Err(IOError::GetMetadataFailed(path.clone(), format!("{result:?}")))?;
Err(IOError::GetMetadataFailed(
path.clone(),
format!("{result:?}"),
))?;
}
Ok::<Permissions, Error>(Permissions {
@ -199,10 +204,19 @@ impl FileSystemTrait for FileSystem {
Ok(permission)
}
async fn set_permission(&self, path: &PathBuf, permissions: Permissions) -> Result<(), Error> {
async fn set_permission(&self, path: &Path, permissions: Permissions) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(IOError::SemaphoreClosed)?;
let file_path_wild: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let security_info = BACKUP_SECURITY_INFORMATION;
let security_info = OWNER_SECURITY_INFORMATION
| GROUP_SECURITY_INFORMATION
| DACL_SECURITY_INFORMATION
| SACL_SECURITY_INFORMATION;
let owner = permissions.owner;
let primary_group = permissions.primary_group;
let dacl = permissions.dacl;
@ -221,7 +235,7 @@ impl FileSystemTrait for FileSystem {
);
if result.is_err() {
Err(IOError::SetMetadataFailed(path.clone(), format!("{result:?}")))?;
Err(IOError::SetMetadataFailed(path, format!("{result:?}")))?;
}
}

39
src/ui/common.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::model::core::backup::backup_execution::BackupExecution;
#[derive(Debug, Clone, PartialEq)]
pub enum PageType {
Executions,
Schedules,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FolderSelectionMode {
Source,
Destination,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ComparisonModeSelection {
Standard,
Advanced,
Thorough,
}
#[derive(Debug, Clone)]
pub struct ExecutionDisplay {
pub execution: BackupExecution,
pub current_folder: String,
pub processed_files: usize,
pub error_count: usize,
}
impl From<BackupExecution> for ExecutionDisplay {
fn from(execution: BackupExecution) -> Self {
Self {
execution,
current_folder: String::new(),
processed_files: 0,
error_count: 0,
}
}
}

View File

@ -1,54 +1,30 @@
use crate::core::app_config::AppConfig;
use crate::core::backup_engine::BackupEngine;
use crate::core::event_bus::EventBus;
use crate::model::backup::backup_execution::*;
use crate::core::backup::backup_service::BackupService;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::model::core::backup::backup_execution::*;
use crate::model::core::backup::message::{
BackupServiceMessage, BackupServiceResponse, ServiceCallMessage, ServiceCallResponse,
};
use crate::model::core::gui::message::GuiMessage;
use crate::model::error::actor::ActorError;
use crate::model::error::Error;
use crate::model::event::error::BackupError;
use crate::model::event::execution::*;
use crate::model::event::filesystem::FolderProcessing;
use crate::ui::common::{ComparisonModeSelection, ExecutionDisplay, FolderSelectionMode};
use dashmap::DashMap;
use eframe::egui;
use egui_file_dialog::FileDialog;
use futures::executor::block_on;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use std::sync::{mpsc, Arc};
use std::time::{Duration, Instant};
use tracing::error;
use uuid::Uuid;
#[derive(Debug, Clone)]
struct ExecutionDisplay {
execution: BackupExecution,
current_folder: String,
processed_files: usize,
error_count: usize,
}
impl From<BackupExecution> for ExecutionDisplay {
fn from(execution: BackupExecution) -> Self {
Self {
execution,
current_folder: String::new(),
processed_files: 0,
error_count: 0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum FolderSelectionMode {
Source,
Destination,
}
pub struct ExecutionPage {
app_config: Arc<AppConfig>,
backup_engine: Arc<BackupEngine>,
actor_system: Arc<ActorSystem>,
folder_processing_events: Receiver<FolderProcessing>,
progress_events: Receiver<ExecutionProgress>,
backup_error_events: Receiver<BackupError>,
message_rx: mpsc::Receiver<GuiMessage>,
executions: DashMap<Uuid, ExecutionDisplay>,
error_messages: DashMap<Uuid, Vec<Error>>,
@ -56,9 +32,10 @@ pub struct ExecutionPage {
new_task_source: String,
new_task_destination: String,
new_task_mirror: bool,
new_task_lock_source: bool,
new_task_backup_permission: bool,
new_task_follow_symlinks: bool,
new_task_comparison_mode: ComparisonModeSelection,
new_task_hash_type: HashType,
show_add_task_dialog: bool,
file_dialog: FileDialog,
@ -73,27 +50,22 @@ pub struct ExecutionPage {
impl ExecutionPage {
pub fn new(
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
actor_system: Arc<ActorSystem>,
message_rx: mpsc::Receiver<GuiMessage>,
) -> Self {
let folder_processing_events = event_bus.subscribe::<FolderProcessing>();
let progress_events = event_bus.subscribe::<ExecutionProgress>();
let backup_error_events = event_bus.subscribe::<BackupError>();
Self {
app_config,
backup_engine,
folder_processing_events,
progress_events,
backup_error_events,
actor_system,
message_rx,
executions: DashMap::new(),
error_messages: DashMap::new(),
new_task_source: String::new(),
new_task_destination: String::new(),
new_task_mirror: false,
new_task_lock_source: false,
new_task_backup_permission: false,
new_task_follow_symlinks: false,
new_task_comparison_mode: ComparisonModeSelection::Standard,
new_task_hash_type: HashType::BLAKE3,
show_add_task_dialog: false,
file_dialog: FileDialog::new(),
folder_selection_mode: None,
@ -105,90 +77,150 @@ impl ExecutionPage {
}
fn process_events(&mut self) {
while let Ok(event) = self.folder_processing_events.try_recv() {
if let Some(mut task_display) = self.executions.get_mut(&event.execution_id) {
task_display.current_folder = event.current_folder.to_string_lossy().to_string();
}
}
while let Ok(event) = self.progress_events.try_recv() {
if let Some(mut task_display) = self.executions.get_mut(&event.task_id) {
task_display.processed_files = event.processed_files;
task_display.error_count = event.error_count;
}
}
while let Ok(event) = self.backup_error_events.try_recv() {
match self.error_messages.get_mut(&event.task_id) {
Some(mut errors) => errors.push(event.error),
None => {
self.error_messages.insert(event.task_id, vec![event.error]);
while let Ok(message) = self.message_rx.try_recv() {
match message {
GuiMessage::FolderProcess { uuid, folder } => {
if let Some(mut task_display) = self.executions.get_mut(&uuid) {
task_display.current_folder = folder.to_string_lossy().to_string();
}
}
GuiMessage::ExecutionProgress {
uuid,
processed_files,
error_count,
} => {
if let Some(mut task_display) = self.executions.get_mut(&uuid) {
task_display.processed_files = processed_files;
task_display.error_count = error_count;
}
}
GuiMessage::ExecutionErrors { uuid, errors } => {
match self.error_messages.get_mut(&uuid) {
Some(mut errors_ref) => errors_ref.extend(errors),
None => {
self.error_messages.insert(uuid, errors);
}
}
}
}
}
}
fn sync_all_execution_states(&mut self) {
let latest_executions = self.backup_engine.get_all_executions();
let latest_ids: std::collections::HashSet<Uuid> = latest_executions.iter().map(|(id, _)| *id).collect();
if let Ok(response) = block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<BackupService>()
.ok_or(ActorError::ActorNotFound)?;
backup_actor_ref
.ask(BackupServiceMessage::ServiceCall(
ServiceCallMessage::GetExecutions,
))
.await
}) {
let BackupServiceResponse::ServiceCall(service_call) = response else {
return;
};
let ServiceCallResponse::GetExecutions(latest_executions) = service_call;
let latest_ids: HashSet<Uuid> = latest_executions.iter().map(|(id, _)| *id).collect();
for (task_id, latest_execution) in latest_executions {
if let Some(mut display) = self.executions.get_mut(&task_id) {
display.execution = latest_execution;
for (task_id, latest_execution) in latest_executions {
if let Some(mut display) = self.executions.get_mut(&task_id) {
display.execution = latest_execution;
}
}
}
self.executions.retain(|task_id, _| latest_ids.contains(task_id));
self.error_messages.retain(|task_id, _| latest_ids.contains(task_id));
self.executions
.retain(|task_id, _| latest_ids.contains(task_id));
self.error_messages
.retain(|task_id, _| latest_ids.contains(task_id));
if let Some(viewing_id) = self.viewing_errors_for_task {
if !latest_ids.contains(&viewing_id) {
self.viewing_errors_for_task = None;
if let Some(viewing_id) = self.viewing_errors_for_task {
if !latest_ids.contains(&viewing_id) {
self.viewing_errors_for_task = None;
}
}
}
}
fn sync_execution_state(&mut self, task_id: Uuid) {
if let Some(latest_execution) = self.backup_engine.get_execution(&task_id) {
if let Some(mut display) = self.executions.get_mut(&task_id) {
display.execution = latest_execution;
}
}
fn handle_add_execution(&mut self, execution: BackupExecution) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<BackupService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
BackupServiceMessage::ServiceCall(ServiceCallMessage::AddExecution(execution));
backup_actor_ref
.tell(message)
.await
.map_err(|_| ActorError::SendMessageError)?;
Ok(())
})
}
fn handle_start_execution(&mut self, task_id: Uuid) {
match block_on(self.backup_engine.start_execution(task_id)) {
Ok(_) => self.sync_execution_state(task_id),
Err(err) => {
self.sync_execution_state(task_id);
error!("{}", err);
}
}
fn handle_start_execution(&mut self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<BackupService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
BackupServiceMessage::ServiceCall(ServiceCallMessage::StartExecution(uuid));
backup_actor_ref
.tell(message)
.await
.map_err(|_| ActorError::SendMessageError)?;
Ok(())
})
}
fn handle_suspend_execution(&mut self, task_id: Uuid) {
match block_on(self.backup_engine.suspend_execution(task_id)) {
Ok(_) => self.sync_execution_state(task_id),
Err(err) => {
self.sync_execution_state(task_id);
error!("{}", err);
}
}
fn handle_suspend_execution(&mut self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<BackupService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
BackupServiceMessage::ServiceCall(ServiceCallMessage::SuspendExecution(uuid));
backup_actor_ref
.tell(message)
.await
.map_err(|_| ActorError::SendMessageError)?;
Ok(())
})
}
fn handle_resume_execution(&mut self, task_id: Uuid) {
match block_on(self.backup_engine.resume_execution(task_id)) {
Ok(_) => self.sync_execution_state(task_id),
Err(err) => {
self.sync_execution_state(task_id);
error!("{}", err);
}
}
fn handle_resume_execution(&mut self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<BackupService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
BackupServiceMessage::ServiceCall(ServiceCallMessage::ResumeExecution(uuid));
backup_actor_ref
.tell(message)
.await
.map_err(|_| ActorError::SendMessageError)?;
Ok(())
})
}
fn handle_remove_execution(&mut self, task_id: Uuid) {
block_on(self.backup_engine.remove_execution(&task_id));
self.executions.remove(&task_id);
fn handle_remove_execution(&mut self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<BackupService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
BackupServiceMessage::ServiceCall(ServiceCallMessage::RemoveExecution(uuid));
backup_actor_ref
.tell(message)
.await
.map_err(|_| ActorError::SendMessageError)?;
Ok(())
})
}
pub fn update(&mut self, ctx: &egui::Context) {
@ -288,7 +320,7 @@ impl ExecutionPage {
fn draw_execution_item(
&mut self,
ui: &mut egui::Ui,
task_id: Uuid,
uuid: Uuid,
task_display: &ExecutionDisplay,
) {
egui::Frame::new()
@ -345,10 +377,10 @@ impl ExecutionPage {
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if let Some(errors) = self.error_messages.get(&task_id) {
if let Some(errors) = self.error_messages.get(&uuid) {
if !errors.is_empty() {
if ui.small_button("👁 View Errors").clicked() {
self.viewing_errors_for_task = Some(task_id);
self.viewing_errors_for_task = Some(uuid);
}
ui.separator();
}
@ -357,24 +389,32 @@ impl ExecutionPage {
match task_display.execution.state {
BackupState::Pending => {
if ui.button("▶ Start").clicked() {
self.handle_start_execution(task_id);
if let Err(err) = self.handle_start_execution(uuid) {
error!("{}", err);
}
}
}
BackupState::Suspended => {
if ui.button("▶ Resume").clicked() {
self.handle_resume_execution(task_id);
if let Err(err) = self.handle_resume_execution(uuid) {
error!("{}", err);
}
}
}
BackupState::Running => {
if ui.button("⏸ Pause").clicked() {
self.handle_suspend_execution(task_id);
if let Err(err) = self.handle_suspend_execution(uuid) {
error!("{}", err);
}
}
}
_ => {}
}
if ui.button("🗑").clicked() {
self.handle_remove_execution(task_id);
if let Err(err) = self.handle_remove_execution(uuid) {
error!("{}", err);
}
}
});
});
@ -392,7 +432,10 @@ impl ExecutionPage {
.spacing([10.0, 4.0])
.show(ui, |ui| {
ui.label("Source Path:");
ui.text_edit_singleline(&mut self.new_task_source);
ui.add_sized(
[300.0, 20.0],
egui::TextEdit::singleline(&mut self.new_task_source),
);
if ui.button("📁 Browse").clicked() {
self.folder_selection_mode = Some(FolderSelectionMode::Source);
self.file_dialog.pick_directory();
@ -400,7 +443,10 @@ impl ExecutionPage {
ui.end_row();
ui.label("Destination Path:");
ui.text_edit_singleline(&mut self.new_task_destination);
ui.add_sized(
[300.0, 20.0],
egui::TextEdit::singleline(&mut self.new_task_destination),
);
if ui.button("📁 Browse").clicked() {
self.folder_selection_mode = Some(FolderSelectionMode::Destination);
self.file_dialog.pick_directory();
@ -410,13 +456,73 @@ impl ExecutionPage {
ui.separator();
ui.label("Options:");
ui.label("File Comparison Mode:");
ui.horizontal(|ui| {
ui.radio_value(
&mut self.new_task_comparison_mode,
ComparisonModeSelection::Standard,
"⚡ Standard (Size + Time)",
);
ui.radio_value(
&mut self.new_task_comparison_mode,
ComparisonModeSelection::Advanced,
"🔧 Advanced (+ Attributes)",
);
ui.radio_value(
&mut self.new_task_comparison_mode,
ComparisonModeSelection::Thorough,
"🔍 Thorough (+ Checksum)",
);
});
if self.new_task_comparison_mode == ComparisonModeSelection::Thorough {
ui.horizontal(|ui| {
ui.label(" Hash Algorithm:");
egui::ComboBox::from_id_salt("hash_type")
.selected_text(format!("{:?}", self.new_task_hash_type))
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.new_task_hash_type,
HashType::BLAKE3,
"BLAKE3 (Recommended)",
);
ui.selectable_value(
&mut self.new_task_hash_type,
HashType::SHA256,
"SHA256",
);
ui.selectable_value(
&mut self.new_task_hash_type,
HashType::SHA3,
"SHA3",
);
ui.selectable_value(
&mut self.new_task_hash_type,
HashType::BLAKE2B,
"BLAKE2B",
);
ui.selectable_value(
&mut self.new_task_hash_type,
HashType::BLAKE2S,
"BLAKE2S",
);
ui.selectable_value(
&mut self.new_task_hash_type,
HashType::MD5,
"MD5 (Legacy)",
);
});
});
}
ui.separator();
ui.label("Additional Options:");
ui.checkbox(&mut self.new_task_follow_symlinks, "Follow Symlinks");
ui.checkbox(
&mut self.new_task_mirror,
"Mirror Mode (Delete extra files in destination)",
);
ui.checkbox(&mut self.new_task_lock_source, "Lock Source Files");
ui.checkbox(
&mut self.new_task_backup_permission,
"Backup File Permissions",
@ -429,25 +535,38 @@ impl ExecutionPage {
&& !self.new_task_source.is_empty()
&& !self.new_task_destination.is_empty()
{
let comparison_mode = match self.new_task_comparison_mode {
ComparisonModeSelection::Standard => Some(ComparisonMode::Standard),
ComparisonModeSelection::Advanced => Some(ComparisonMode::Advanced),
ComparisonModeSelection::Thorough => {
Some(ComparisonMode::Thorough(self.new_task_hash_type))
}
};
let execution = BackupExecution {
uuid: Uuid::new_v4(),
state: BackupState::Pending,
source_path: PathBuf::from(&self.new_task_source),
destination_path: PathBuf::from(&self.new_task_destination),
backup_type: BackupType::Full,
comparison_mode: None,
comparison_mode,
options: BackupOptions {
mirror: self.new_task_mirror,
lock_source: self.new_task_lock_source,
backup_permission: self.new_task_backup_permission,
follow_symlinks: self.new_task_follow_symlinks,
},
};
block_on(self.backup_engine.add_execution(execution.clone()));
let execution_display = ExecutionDisplay::from(execution.clone());
self.executions.insert(execution.uuid, execution_display);
self.reset_form();
match self.handle_add_execution(execution.clone()) {
Ok(_) => {
let execution_display = ExecutionDisplay::from(execution.clone());
self.executions.insert(execution.uuid, execution_display);
self.reset_form();
}
Err(err) => {
error!("{}", err);
}
}
}
if ui.button("Cancel").clicked() {
@ -559,9 +678,10 @@ impl ExecutionPage {
self.new_task_source.clear();
self.new_task_destination.clear();
self.new_task_mirror = false;
self.new_task_lock_source = false;
self.new_task_backup_permission = false;
self.new_task_follow_symlinks = false;
self.new_task_comparison_mode = ComparisonModeSelection::Standard;
self.new_task_hash_type = HashType::BLAKE3;
self.show_add_task_dialog = false;
}
}

View File

@ -1,20 +1,10 @@
use crate::core::backup_engine::BackupEngine;
use crate::core::event_bus::EventBus;
use crate::core::schedule_manager::ScheduleManager;
use crate::model::log::system::SystemLog;
use crate::ui::common::PageType;
use crate::ui::execution_page::ExecutionPage;
use crate::ui::schedule_page::SchedulePage;
use crate::model::log::system::SystemLog;
use eframe::egui;
use eframe::{App, Frame};
use macros::log;
use std::sync::Arc;
use crate::core::app_config::AppConfig;
#[derive(Debug, Clone, PartialEq)]
enum PageType {
Executions,
Schedules,
}
pub struct MainPage {
current_page: PageType,
@ -23,37 +13,38 @@ pub struct MainPage {
}
impl MainPage {
pub fn new(
app_config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
schedule_manager: Arc<ScheduleManager>,
) -> Self {
pub fn new(execution_page: ExecutionPage, schedule_page: SchedulePage) -> Self {
Self {
current_page: PageType::Executions,
execution_page: ExecutionPage::new(app_config.clone(), event_bus, backup_engine),
schedule_page: SchedulePage::new(app_config, schedule_manager),
execution_page,
schedule_page,
}
}
fn draw_top_panel(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Exit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("View", |ui| {
match self.current_page {
PageType::Executions => {
ui.checkbox(&mut self.execution_page.show_completed_tasks, "Show Completed Tasks");
ui.checkbox(&mut self.execution_page.auto_scroll_errors, "Auto-scroll Error Messages");
}
PageType::Schedules => {
ui.checkbox(&mut self.schedule_page.show_disabled_schedules, "Show Disabled Schedules");
}
ui.menu_button("View", |ui| match self.current_page {
PageType::Executions => {
ui.checkbox(
&mut self.execution_page.show_completed_tasks,
"Show Completed Tasks",
);
ui.checkbox(
&mut self.execution_page.auto_scroll_errors,
"Auto-scroll Error Messages",
);
}
PageType::Schedules => {
ui.checkbox(
&mut self.schedule_page.show_disabled_schedules,
"Show Disabled Schedules",
);
}
});
});
@ -63,7 +54,11 @@ impl MainPage {
fn draw_tabs(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("tabs_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.current_page, PageType::Executions, "📋 Executions");
ui.selectable_value(
&mut self.current_page,
PageType::Executions,
"📋 Executions",
);
ui.selectable_value(&mut self.current_page, PageType::Schedules, "⏰ Schedules");
});
});

View File

@ -1,3 +1,4 @@
pub mod common;
pub mod execution_page;
pub mod main_page;
pub mod schedule_page;

View File

@ -1,26 +1,27 @@
use crate::core::app_config::AppConfig;
use crate::core::schedule_manager::ScheduleManager;
use crate::model::backup::backup_execution::*;
use crate::model::backup::backup_schedule::*;
use crate::model::error::task::TaskError;
use crate::core::infrastructure::actor_system::ActorSystem;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::schedule::schedule_service::ScheduleService;
use crate::model::core::actor::actor_ref::ActorRef;
use crate::model::core::backup::backup_execution::*;
use crate::model::core::schedule::backup_schedule::*;
use crate::model::core::schedule::message::*;
use crate::model::error::actor::ActorError;
use crate::model::error::Error;
use crate::ui::common::{ComparisonModeSelection, FolderSelectionMode};
use eframe::egui;
use egui_file_dialog::FileDialog;
use futures::executor::block_on;
use macros::log;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::error;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq)]
enum FolderSelectionMode {
Source,
Destination,
}
pub struct SchedulePage {
app_config: Arc<AppConfig>,
schedule_manager: Arc<ScheduleManager>,
actor_system: Arc<ActorSystem>,
schedule_service_ref: ActorRef<ScheduleServiceMessage>,
schedules: Vec<BackupSchedule>,
@ -29,9 +30,10 @@ pub struct SchedulePage {
new_schedule_destination: String,
new_schedule_interval: ScheduleInterval,
new_schedule_mirror: bool,
new_schedule_lock_source: bool,
new_schedule_backup_permission: bool,
new_schedule_follow_symlinks: bool,
new_schedule_comparison_mode: ComparisonModeSelection,
new_schedule_hash_type: HashType,
show_add_schedule_dialog: bool,
file_dialog: FileDialog,
@ -43,35 +45,132 @@ pub struct SchedulePage {
}
impl SchedulePage {
pub fn new(app_config: Arc<AppConfig>, schedule_manager: Arc<ScheduleManager>) -> Self {
Self {
pub fn new(app_config: Arc<AppConfig>, actor_system: Arc<ActorSystem>) -> Result<Self, Error> {
let schedule_service_ref = actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let schedule_page = Self {
app_config,
schedule_manager,
actor_system,
schedule_service_ref,
schedules: Vec::new(),
new_schedule_name: String::new(),
new_schedule_source: String::new(),
new_schedule_destination: String::new(),
new_schedule_interval: ScheduleInterval::Daily,
new_schedule_mirror: false,
new_schedule_lock_source: false,
new_schedule_backup_permission: false,
new_schedule_follow_symlinks: false,
new_schedule_comparison_mode: ComparisonModeSelection::Standard,
new_schedule_hash_type: HashType::BLAKE3,
show_add_schedule_dialog: false,
file_dialog: FileDialog::new(),
folder_selection_mode: None,
show_disabled_schedules: true,
viewing_schedule_details: None,
last_refresh: None,
}
};
Ok(schedule_page)
}
fn load_schedules(&mut self) {
match block_on(self.schedule_manager.get_all_schedules()) {
Ok(schedules) => self.schedules = schedules,
Err(err) => log!(TaskError::LoadScheduleFailed(err)),
match block_on(async {
self.schedule_service_ref
.ask(ScheduleServiceMessage::ServiceCall(
ServiceCallMessage::GetSchedules,
))
.await
}) {
Ok(ScheduleServiceResponse::ServiceCall(ServiceCallResponse::GetSchedules(
schedules,
))) => {
self.schedules = schedules;
}
Ok(ScheduleServiceResponse::None) => {}
Err(err) => {
error!("{}", err);
}
}
}
fn handle_add_schedule(&self, schedule: BackupSchedule) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
ScheduleServiceMessage::ServiceCall(ServiceCallMessage::AddSchedule(schedule));
backup_actor_ref.tell(message).await?;
Ok(())
})
}
fn handle_modify_schedule(&self, schedule: BackupSchedule) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
ScheduleServiceMessage::ServiceCall(ServiceCallMessage::ModifySchedule(schedule));
backup_actor_ref.tell(message).await?;
Ok(())
})
}
fn handle_remove_schedule(&self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
ScheduleServiceMessage::ServiceCall(ServiceCallMessage::RemoveSchedule(uuid));
backup_actor_ref.tell(message).await?;
Ok(())
})
}
fn handle_active_schedule(&self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
ScheduleServiceMessage::ServiceCall(ServiceCallMessage::ActivateSchedule(uuid));
backup_actor_ref.tell(message).await?;
Ok(())
})
}
fn handle_pause_schedule(&self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
ScheduleServiceMessage::ServiceCall(ServiceCallMessage::PauseSchedule(uuid));
backup_actor_ref.tell(message).await?;
Ok(())
})
}
fn handle_disable_schedule(&self, uuid: Uuid) -> Result<(), Error> {
block_on(async {
let backup_actor_ref = self
.actor_system
.actor_of::<ScheduleService>()
.ok_or(ActorError::ActorNotFound)?;
let message =
ScheduleServiceMessage::ServiceCall(ServiceCallMessage::DisableSchedule(uuid));
backup_actor_ref.tell(message).await?;
Ok(())
})
}
pub fn update(&mut self, ctx: &egui::Context) {
let should_refresh = match self.last_refresh {
None => true,
@ -165,17 +264,27 @@ impl SchedulePage {
ui.label(format!("📅 {}", schedule.name));
ui.label(format!("🗂️ {}", schedule.source_path.display()));
ui.label(format!("📁 {}", schedule.destination_path.display()));
ui.label(format!(" {:?}", schedule.interval));
ui.label(format!(" {:?}", schedule.interval));
ui.horizontal(|ui| {
let (color, symbol, status_text) = match schedule.state {
ScheduleState::Active => (egui::Color32::GREEN, "", "Active"),
ScheduleState::Paused => (egui::Color32::YELLOW, "", "Paused"),
ScheduleState::Paused => (egui::Color32::YELLOW, "", "Paused"),
ScheduleState::Disabled => (egui::Color32::GRAY, "", "Disabled"),
};
ui.colored_label(color, format!("{symbol} {status_text}"));
if let Some(comparison_mode) = &schedule.comparison_mode {
ui.separator();
let mode_text = match comparison_mode {
ComparisonMode::Standard => "⚡ Standard",
ComparisonMode::Advanced => "🔧 Advanced",
ComparisonMode::Thorough(_) => "🔍 Thorough",
};
ui.label(mode_text);
}
if let Some(last_run) = schedule.last_run_time {
ui.separator();
ui.label(format!(
@ -203,29 +312,23 @@ impl SchedulePage {
match schedule.state {
ScheduleState::Active => {
if ui.button("⏸️ Pause").clicked() {
if let Err(err) = block_on(
self.schedule_manager.pause_schedule(schedule.uuid),
) {
log!(TaskError::PauseScheduleFailed(err));
if ui.button("⏸ Pause").clicked() {
if let Err(err) = self.handle_pause_schedule(schedule.uuid) {
error!("{}", err);
}
}
}
ScheduleState::Paused => {
if ui.button("▶️ Resume").clicked() {
if let Err(err) = block_on(
self.schedule_manager.active_schedule(schedule.uuid),
) {
log!(TaskError::EnableScheduleFailed(err));
if ui.button("▶ Resume").clicked() {
if let Err(err) = self.handle_active_schedule(schedule.uuid) {
error!("{}", err);
}
}
}
ScheduleState::Disabled => {
if ui.button("▶️ Enable").clicked() {
if let Err(err) = block_on(
self.schedule_manager.active_schedule(schedule.uuid),
) {
log!(TaskError::EnableScheduleFailed(err));
if ui.button("▶ Enable").clicked() {
if let Err(err) = self.handle_active_schedule(schedule.uuid) {
error!("{}", err);
}
}
}
@ -234,18 +337,14 @@ impl SchedulePage {
if schedule.state != ScheduleState::Disabled
&& ui.button("❌ Disable").clicked()
{
if let Err(err) =
block_on(self.schedule_manager.disable_schedule(schedule.uuid))
{
log!(TaskError::DisableScheduleFailed(err));
if let Err(err) = self.handle_disable_schedule(schedule.uuid) {
error!("{}", err);
}
}
if ui.button("🗑️").clicked() {
if let Err(err) =
block_on(self.schedule_manager.remove_schedule(schedule.uuid))
{
log!(TaskError::RemoveScheduleFailed(err));
if ui.button("🗑").clicked() {
if let Err(err) = self.handle_remove_schedule(schedule.uuid) {
error!("{}", err);
}
}
});
@ -264,10 +363,35 @@ impl SchedulePage {
.spacing([10.0, 4.0])
.show(ui, |ui| {
ui.label("Schedule Name:");
ui.text_edit_singleline(&mut self.new_schedule_name);
ui.add_sized(
[300.0, 20.0],
egui::TextEdit::singleline(&mut self.new_schedule_name),
);
ui.label("");
ui.end_row();
ui.label("Source Path:");
ui.add_sized(
[300.0, 20.0],
egui::TextEdit::singleline(&mut self.new_schedule_source),
);
if ui.button("📁 Browse").clicked() {
self.folder_selection_mode = Some(FolderSelectionMode::Source);
self.file_dialog.pick_directory();
}
ui.end_row();
ui.label("Destination Path:");
ui.add_sized(
[300.0, 20.0],
egui::TextEdit::singleline(&mut self.new_schedule_destination),
);
if ui.button("📁 Browse").clicked() {
self.folder_selection_mode = Some(FolderSelectionMode::Destination);
self.file_dialog.pick_directory();
}
ui.end_row();
ui.label("Interval:");
egui::ComboBox::from_label("")
.selected_text(format!("{:?}", self.new_schedule_interval))
@ -295,33 +419,77 @@ impl SchedulePage {
});
ui.label("");
ui.end_row();
ui.label("Source Path:");
ui.text_edit_singleline(&mut self.new_schedule_source);
if ui.button("📁 Browse").clicked() {
self.folder_selection_mode = Some(FolderSelectionMode::Source);
self.file_dialog.pick_directory();
}
ui.end_row();
ui.label("Destination Path:");
ui.text_edit_singleline(&mut self.new_schedule_destination);
if ui.button("📁 Browse").clicked() {
self.folder_selection_mode = Some(FolderSelectionMode::Destination);
self.file_dialog.pick_directory();
}
ui.end_row();
});
ui.separator();
ui.label("Options:");
ui.label("File Comparison Mode:");
ui.horizontal(|ui| {
ui.radio_value(
&mut self.new_schedule_comparison_mode,
ComparisonModeSelection::Standard,
"⚡ Standard (Size + Time)",
);
ui.radio_value(
&mut self.new_schedule_comparison_mode,
ComparisonModeSelection::Advanced,
"🔧 Advanced (+ Attributes)",
);
ui.radio_value(
&mut self.new_schedule_comparison_mode,
ComparisonModeSelection::Thorough,
"🔍 Thorough (+ Checksum)",
);
});
if self.new_schedule_comparison_mode == ComparisonModeSelection::Thorough {
ui.horizontal(|ui| {
ui.label(" Hash Algorithm:");
egui::ComboBox::from_id_salt("schedule_hash_type")
.selected_text(format!("{:?}", self.new_schedule_hash_type))
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.new_schedule_hash_type,
HashType::BLAKE3,
"BLAKE3 (Recommended)",
);
ui.selectable_value(
&mut self.new_schedule_hash_type,
HashType::SHA256,
"SHA256",
);
ui.selectable_value(
&mut self.new_schedule_hash_type,
HashType::SHA3,
"SHA3",
);
ui.selectable_value(
&mut self.new_schedule_hash_type,
HashType::BLAKE2B,
"BLAKE2B",
);
ui.selectable_value(
&mut self.new_schedule_hash_type,
HashType::BLAKE2S,
"BLAKE2S",
);
ui.selectable_value(
&mut self.new_schedule_hash_type,
HashType::MD5,
"MD5 (Legacy)",
);
});
});
}
ui.separator();
ui.label("Additional Options:");
ui.checkbox(&mut self.new_schedule_follow_symlinks, "Follow Symlinks");
ui.checkbox(
&mut self.new_schedule_mirror,
"Mirror Mode (Delete extra files in destination)",
);
ui.checkbox(&mut self.new_schedule_lock_source, "Lock Source Files");
ui.checkbox(
&mut self.new_schedule_backup_permission,
"Backup File Permissions",
@ -335,6 +503,14 @@ impl SchedulePage {
&& !self.new_schedule_source.is_empty()
&& !self.new_schedule_destination.is_empty()
{
let comparison_mode = match self.new_schedule_comparison_mode {
ComparisonModeSelection::Standard => Some(ComparisonMode::Standard),
ComparisonModeSelection::Advanced => Some(ComparisonMode::Advanced),
ComparisonModeSelection::Thorough => {
Some(ComparisonMode::Thorough(self.new_schedule_hash_type))
}
};
let schedule = BackupSchedule {
uuid: Uuid::new_v4(),
name: self.new_schedule_name.clone(),
@ -342,10 +518,9 @@ impl SchedulePage {
source_path: PathBuf::from(&self.new_schedule_source),
destination_path: PathBuf::from(&self.new_schedule_destination),
backup_type: BackupType::Full,
comparison_mode: None,
comparison_mode,
options: BackupOptions {
mirror: self.new_schedule_mirror,
lock_source: self.new_schedule_lock_source,
backup_permission: self.new_schedule_backup_permission,
follow_symlinks: self.new_schedule_follow_symlinks,
},
@ -356,10 +531,8 @@ impl SchedulePage {
updated_at: chrono::Utc::now().naive_utc(),
};
if let Err(e) =
block_on(self.schedule_manager.create_schedule(schedule))
{
eprintln!("Failed to create schedule: {e:?}");
if let Err(err) = self.handle_add_schedule(schedule) {
error!("{}", err);
}
self.reset_schedule_form();
@ -424,6 +597,19 @@ impl SchedulePage {
ui.label(format!("{:?}", schedule.backup_type));
ui.end_row();
if let Some(comparison_mode) = &schedule.comparison_mode {
ui.label("Comparison Mode:");
let mode_text = match comparison_mode {
ComparisonMode::Standard => "Standard (Size + Time)",
ComparisonMode::Advanced => "Advanced (+ Attributes)",
ComparisonMode::Thorough(hash_type) => {
&format!("Thorough (+ Checksum: {hash_type:?})")
}
};
ui.label(mode_text);
ui.end_row();
}
ui.label("Interval:");
ui.label(format!("{:?}", schedule.interval));
ui.end_row();
@ -460,9 +646,6 @@ impl SchedulePage {
if schedule.options.mirror {
ui.label("✅ Mirror Mode");
}
if schedule.options.lock_source {
ui.label("✅ Lock Source");
}
if schedule.options.backup_permission {
ui.label("✅ Backup Permissions");
}
@ -475,13 +658,9 @@ impl SchedulePage {
ui.horizontal(|ui| {
if ui.button("Run Now").clicked() {
// 這裡可以觸發立即執行排程
// 可能需要新增一個 API 方法來立即執行排程
println!("Would run schedule {} now", schedule.name);
}
if ui.button("Edit").clicked() {
// 這裡可以開啟編輯對話框
println!("Would edit schedule {}", schedule.name);
}
});
@ -500,9 +679,10 @@ impl SchedulePage {
self.new_schedule_destination.clear();
self.new_schedule_interval = ScheduleInterval::Daily;
self.new_schedule_mirror = false;
self.new_schedule_lock_source = false;
self.new_schedule_backup_permission = false;
self.new_schedule_follow_symlinks = false;
self.new_schedule_comparison_mode = ComparisonModeSelection::Standard;
self.new_schedule_hash_type = HashType::BLAKE3;
self.show_add_schedule_dialog = false;
}
}

View File

@ -4,6 +4,7 @@ use crate::platform::constants::DATABASE_LOCK_PATH;
use std::fs;
use tokio::fs::File;
#[derive(Debug)]
pub struct DatabaseLock {
_private: (),
}

View File

@ -1,35 +0,0 @@
use crate::model::error::io::IOError;
use crate::model::error::Error;
use fs4::tokio::AsyncFileExt;
use macros::log;
use std::path::PathBuf;
use tokio::fs::File;
#[derive(Debug)]
pub struct FileLock {
file: File,
path: PathBuf,
}
impl FileLock {
pub async fn new(path: &PathBuf) -> Result<Self, Error> {
let file = File::open(path)
.await
.map_err(|err| IOError::ReadFileFailed(path.clone(), err))?;
file.try_lock_exclusive()
.map_err(|err| IOError::LockFileFailed(path.clone(), err))?;
Ok(Self {
file,
path: path.clone(),
})
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let path = self.path.clone();
if let Err(err) = self.file.unlock() {
log!(IOError::UnlockFileFailed(path, err));
}
}
}

View File

@ -1,6 +1,5 @@
pub mod assets;
pub mod database_lock;
pub mod file_hash;
pub mod file_lock;
pub mod font;
pub mod logging;