feat: Complete core backup system implementation with scheduling and GUI (#1)

* refactor: Refactor logging and error handling structures

* refactor: Refactor logging and error handling across modules

* feat: Complete unix filesystem abstract

* feat: Complete backup algorithm

* fix: Invert logic for `need_copy` in comparison mode

* refactor: Refactor event system and enhance file handling.

* feat: Implement file locking and symlink handling logic.

* fix: Fix the wrong function argument

* feat: Add progress tracking and task state handling for backups

* chore: Update dependent crates

* feat: Remove anyhow to improve performance and progress tracker design

* wip: Remove singleton pattern

* feat: Remove all of singleton pattern

* wip: prepare for slint gui

* chore: Update dependent crates

* wip: UI-related module is not yet completed

* wip: Refactor logging macros

* refactor: Add some custom proc macro to improve error handling

* feat: Update dependent and complete demo gui frame

* refactor: Remove database_ops module and add the task schedule into database

* feat: Introduce task scheduling with new schedule manager and backup structure

* refactor: Update error handling and logging

* feat: Add schedule and execution ui page
This commit is contained in:
DaLaw2 2025-07-20 18:28:19 +08:00 committed by GitHub
parent 4bbb0e1d3d
commit c14fd32277
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 7298 additions and 1723 deletions

4
.gitignore vendored
View File

@ -1,5 +1,5 @@
/target
/logs
target
logs
.idea
mirrorSphere.db
db.lock

3297
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,41 @@
[package]
name = "MirrorSphere"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
log = "0.4.27"
toml = "0.8.22"
anyhow = "1.0.98"
dashmap = "6.1.0"
tracing = "0.1.41"
privilege = "0.3.0"
thiserror = "2.0.12"
tracing-appender = "0.2.3"
fs4 = { version = "0.13.1", features = ["tokio"] }
tokio = { version = "1.45.0", features = ["full"] }
serde = { version = "1.0.219", features = ["derive"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "uuid", "derive"] }
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
serde_json = "1.0.140"
futures = "0.3.31"
async-trait = "0.1.88"
tokio-stream = "0.1.17"
sha3 = "0.10.8"
sha2 = "0.10.8"
bincode = { version = "2.0.1", features = ["serde"] }
blake2 = "0.10.6"
blake3 = { version = "1.8.2", features = ["traits-preview"] }
chrono = { version = "0.4.41", features = ["serde"] }
crossbeam-queue = "0.3.11"
dashmap = "6.1.0"
digest = "0.10.7"
eframe = "0.32.0"
egui-file-dialog = "0.11.0"
fs4 = { version = "0.13.1", features = ["sync", "tokio"] }
futures = "0.3.31"
libc = "0.2.169"
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
macros = { path = "macros" }
md-5 = "0.10.6"
chrono = "0.4.41"
[dependencies.blake3]
version = "1.8.2"
features = ["traits-preview"]
[dependencies.uuid]
version = "1.16.0"
features = ["v4", "fast-rng", "serde"]
memmap2 = "0.9.5"
paste = "1.0.15"
privilege = "0.3.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sha2 = "0.10.8"
sha3 = "0.10.8"
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "uuid", "derive", "chrono"] }
thiserror = "2.0.12"
tokio-stream = "0.1.17"
tokio = { version = "1.45.1", features = ["full"] }
toml = "0.8.22"
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
uuid = { version = "1.17.0", features = ["v4", "fast-rng", "serde"] }
[target.'cfg(windows)'.dependencies.windows-acl]
version = "0.3.0"
@ -43,7 +43,7 @@ version = "0.3.0"
[target.'cfg(windows)'.dependencies.windows]
version = "0.61.1"
features = [
"Win32", "Win32_System", "Win32_System_Memory", "Win32_System_Threading", "Win32_System_Time", "Win32_System_Registry",
"Win32_System_Com", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage", "Win32_Storage_FileSystem",
"Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_Foundation"
"Win32", "Win32_System", "Win32_System_Memory", "Win32_System_IO", "Win32_System_Threading", "Win32_System_Time",
"Win32_System_Registry", "Win32_System_Com", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage",
"Win32_Storage_FileSystem", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_Foundation"
]

View File

@ -1,3 +1,5 @@
[Config]
internal_timestamp = 20
default_wakeup_time = 10
max_concurrency = 16
max_file_operations = 100

View File

@ -1,10 +1,5 @@
TODO
- 使用執行緒安全的 queue 來實作演算法 可以實現演算法的平行化
一些想法 (皆需要直接調用 Windows API)
一些想法 (需要直接調用 Windows API)
- 用 USN Journal 來追蹤檔案系統變更來達到快速掃描的目的 (NTFS 限定)
- 支援 Windows NTFS ACL (Access Control List)
- 支援額外的檔案跟檔案屬性
平行化版本
Initialize:
@ -23,9 +18,6 @@ for i in 1..=worker_count:
queue.extend(current_level_visited.clear())
continue
if need_resume && not reach_checkpoint:
continue
process current_dir:
- check/create directory structure
- spawn file copy tasks
@ -43,6 +35,6 @@ for i in 1..=worker_count:
queue.push(sub_dir)
update progress
update progress (save queue, set state)
save checkpoint periodically
join all workers

12
macros/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

20
macros/src/lib.rs Normal file
View File

@ -0,0 +1,20 @@
mod log;
mod loggable;
mod traceable;
use proc_macro::TokenStream;
#[proc_macro]
pub fn log(input: TokenStream) -> TokenStream {
log::log_impl(input)
}
#[proc_macro]
pub fn loggable(input: TokenStream) -> TokenStream {
loggable::loggable_impl(input)
}
#[proc_macro]
pub fn traceable(input: TokenStream) -> TokenStream {
traceable::traceable_impl(input)
}

66
macros/src/log.rs Normal file
View File

@ -0,0 +1,66 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, Expr, Token};
struct LogInput {
error: Expr,
debug_info: Option<Expr>,
}
impl Parse for LogInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let error = input.parse::<Expr>()?;
let debug_info = if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
Some(input.parse::<Expr>()?)
} else {
None
};
Ok(LogInput { error, debug_info })
}
}
pub fn log_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LogInput);
let error_expr = &input.error;
if let Some(debug_info) = &input.debug_info {
quote! {
{
let error = #error_expr;
let level = error.level();
let message = error.to_string();
let debug_info = #debug_info;
match level {
tracing::Level::ERROR => tracing::error!(message = %message, debug = ?debug_info),
tracing::Level::WARN => tracing::warn!(message = %message, debug = ?debug_info),
tracing::Level::INFO => tracing::info!(message = %message, debug = ?debug_info),
tracing::Level::DEBUG => tracing::debug!(message = %message, debug = ?debug_info),
tracing::Level::TRACE => tracing::trace!(message = %message, debug = ?debug_info),
}
}
}
} else {
quote! {
{
let error = #error_expr;
let level = error.level();
let message = error.to_string();
match level {
tracing::Level::ERROR => tracing::error!("{}", message),
tracing::Level::WARN => tracing::warn!("{}", message),
tracing::Level::INFO => tracing::info!("{}", message),
tracing::Level::DEBUG => tracing::debug!("{}", message),
tracing::Level::TRACE => tracing::trace!("{}", message),
}
}
}
}
.into()
}

143
macros/src/loggable.rs Normal file
View File

@ -0,0 +1,143 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{braced, parse_macro_input, Attribute, Fields, Ident, LitStr, Token, Type, Visibility};
struct LoggableVariant {
error_message: LitStr,
name: Ident,
fields: Fields,
level: syn::Expr,
}
struct LoggableInput {
enum_name: Ident,
variants: Vec<LoggableVariant>,
}
impl Parse for LoggableInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let enum_name = input.parse::<Ident>()?;
let content;
braced!(content in input);
let mut variants = Vec::new();
while !content.is_empty() {
let attrs = content.call(Attribute::parse_outer)?;
let error_attr = attrs
.iter()
.find(|attr| attr.path().is_ident("error"))
.ok_or_else(|| content.error("Expected #[error(...)] attribute"))?;
let error_message = error_attr.parse_args::<LitStr>()?;
let name = content.parse::<Ident>()?;
let fields = if content.peek(syn::token::Brace) {
let field_content;
braced!(field_content in content);
let mut named_fields = syn::punctuated::Punctuated::new();
while !field_content.is_empty() {
let field_name = field_content.parse::<Ident>()?;
field_content.parse::<Token![:]>()?;
let field_type = field_content.parse::<Type>()?;
named_fields.push(syn::Field {
attrs: vec![],
vis: Visibility::Inherited,
mutability: syn::FieldMutability::None,
ident: Some(field_name),
colon_token: Some(Default::default()),
ty: field_type,
});
if field_content.peek(Token![,]) {
field_content.parse::<Token![,]>()?;
}
}
Fields::Named(syn::FieldsNamed {
brace_token: Default::default(),
named: named_fields,
})
} else {
Fields::Unit
};
content.parse::<Token![=>]>()?;
let level = content.parse::<syn::Expr>()?;
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
variants.push(LoggableVariant {
error_message,
name,
fields,
level,
});
}
Ok(LoggableInput {
enum_name,
variants,
})
}
}
pub fn loggable_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LoggableInput);
let enum_name = &input.enum_name;
let variants = &input.variants;
let enum_variants = variants.iter().map(|variant| {
let name = &variant.name;
let error_attr = &variant.error_message;
let fields = &variant.fields;
quote! {
#[error(#error_attr)]
#name #fields
}
});
let level_match_arms = variants.iter().map(|variant| {
let name = &variant.name;
let level = &variant.level;
let field_pattern = match &variant.fields {
Fields::Unit => quote! {},
Fields::Named(fields) => {
let field_names = fields.named.iter().map(|f| &f.ident);
quote! { { #(#field_names: _),* } }
}
Fields::Unnamed(_) => quote! { (..) },
};
quote! {
Self::#name #field_pattern => #level
}
});
quote! {
#[allow(dead_code)]
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
pub enum #enum_name {
#(#enum_variants,)*
}
impl #enum_name {
#[allow(dead_code)]
pub fn level(&self) -> tracing::Level {
match self {
#(#level_match_arms,)*
}
}
}
}
.into()
}

231
macros/src/traceable.rs Normal file
View File

@ -0,0 +1,231 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream}, parse_macro_input, spanned::Spanned, Attribute, Error, Expr, Ident, LitStr,
Result,
Token,
Type,
};
struct ErrorVariant {
attributes: Vec<Attribute>,
error_msg: LitStr,
name: Ident,
fields: Vec<(Ident, Type)>,
level: Expr,
}
impl ErrorVariant {
fn has_no_source(&self) -> bool {
self.attributes
.iter()
.any(|attr| attr.path().is_ident("no_source"))
}
fn should_generate_constructor(&self) -> bool {
if self.has_no_source() {
!self.fields.is_empty()
} else {
true
}
}
}
struct TraceableInput {
enum_name: Ident,
variants: Vec<ErrorVariant>,
}
impl Parse for TraceableInput {
fn parse(input: ParseStream) -> Result<Self> {
let enum_name = input.parse::<Ident>()?;
let content;
syn::braced!(content in input);
let mut variants = Vec::new();
while !content.is_empty() {
let mut attributes = Vec::new();
while content.peek(Token![#]) {
attributes.push(content.call(Attribute::parse_outer)?);
}
let attributes: Vec<_> = attributes.into_iter().flatten().collect();
let error_attr = attributes
.iter()
.find(|attr| attr.path().is_ident("error"))
.ok_or_else(|| Error::new(content.span(), "Missing #[error] attribute"))?;
let error_msg = match &error_attr.meta {
syn::Meta::List(list) => syn::parse2::<LitStr>(list.tokens.clone())?,
_ => {
return Err(Error::new(
error_attr.span(),
"Invalid error attribute format",
));
}
};
let name = content.parse::<Ident>()?;
let mut fields = Vec::new();
if content.peek(syn::token::Brace) {
let fields_content;
syn::braced!(fields_content in content);
while !fields_content.is_empty() {
let field_name = fields_content.parse::<Ident>()?;
fields_content.parse::<Token![:]>()?;
let field_type = fields_content.parse::<Type>()?;
fields.push((field_name, field_type));
if !fields_content.is_empty() {
fields_content.parse::<Token![,]>()?;
}
}
}
content.parse::<Token![=>]>()?;
let level = content.parse::<Expr>()?;
if !content.is_empty() {
content.parse::<Token![,]>()?;
}
variants.push(ErrorVariant {
attributes,
error_msg,
name,
fields,
level,
});
}
Ok(TraceableInput {
enum_name,
variants,
})
}
}
pub fn traceable_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as TraceableInput);
let enum_name = &input.enum_name;
let variants = &input.variants;
let enum_variants = variants.iter().map(|variant| {
let name = &variant.name;
let error_msg = &variant.error_msg;
let fields = &variant.fields;
let field_definitions = fields.iter().map(|(name, ty)| {
quote! { #name: #ty }
});
if variant.has_no_source() {
if variant.fields.is_empty() {
quote! {
#[error(#error_msg)]
#name
}
} else {
quote! {
#[error(#error_msg)]
#name { #(#field_definitions,)* }
}
}
} else {
quote! {
#[error(#error_msg)]
#name {
#(#field_definitions,)*
err: String
}
}
}
});
let level_match_arms = variants.iter().map(|variant| {
let name = &variant.name;
let level = &variant.level;
if variant.has_no_source() {
if variant.fields.is_empty() {
quote! {
Self::#name => #level
}
} else {
quote! {
Self::#name { .. } => #level
}
}
} else {
quote! {
Self::#name { err: _, .. } => #level
}
}
});
let constructors = variants.iter().filter_map(|variant| {
if !variant.should_generate_constructor() {
return None;
}
let name = &variant.name;
let fields = &variant.fields;
let params = fields.iter().map(|(field_name, field_type)| {
quote! { #field_name: impl Into<#field_type> }
});
let field_assignments = fields.iter().map(|(field_name, _)| {
quote! { #field_name: #field_name.into() }
});
if variant.has_no_source() {
Some(quote! {
#[allow(non_snake_case)]
pub fn #name(#(#params),*) -> Self {
Self::#name {
#(#field_assignments,)*
}
}
})
} else {
Some(quote! {
#[allow(non_snake_case)]
pub fn #name(#(#params,)* source: impl std::fmt::Display) -> Self {
Self::#name {
#(#field_assignments,)*
err: source.to_string()
}
}
})
}
});
let expanded = quote! {
#[allow(dead_code)]
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
pub enum #enum_name {
#(#enum_variants,)*
}
impl #enum_name {
#[allow(dead_code)]
pub fn level(&self) -> tracing::Level {
match self {
#(#level_match_arms,)*
}
}
#(#constructors)*
}
};
TokenStream::from(expanded)
}

View File

@ -1,55 +1,37 @@
use crate::model::config::{Config, ConfigTable};
use crate::utils::log_entry::system::SystemEntry;
use crate::model::error::system::SystemError;
use crate::model::error::Error;
use crate::model::log::system::SystemLog;
use macros::log;
use std::fs;
use std::sync::{OnceLock, RwLock as SyncRwLock};
use tokio::sync::RwLock as AsyncRwLock;
use tracing::info;
use std::ops::Deref;
static SYNC_CONFIG: OnceLock<SyncRwLock<Config>> = OnceLock::new();
static ASYNC_CONFIG: OnceLock<AsyncRwLock<Config>> = OnceLock::new();
pub struct AppConfig;
pub struct AppConfig {
config: Config,
}
impl AppConfig {
pub async fn initialization() {
info!("{}", SystemEntry::Initializing);
let config = Self::load_config();
SYNC_CONFIG.get_or_init(|| SyncRwLock::new(config.clone()));
ASYNC_CONFIG.get_or_init(move || AsyncRwLock::new(config));
info!("{}", SystemEntry::InitializeComplete);
pub fn new() -> Result<Self, Error> {
log!(SystemLog::Initializing);
let config = Self::load_config_file()?;
log!(SystemLog::InitializeComplete);
Ok(Self { config })
}
fn load_config() -> Config {
let config = match fs::read_to_string("./config.toml") {
Ok(toml_string) => match toml::from_str::<ConfigTable>(&toml_string) {
Ok(config_table) => config_table.config,
Err(_) => panic!("{}", SystemEntry::InvalidConfig)
}
Err(_) => panic!("{}", SystemEntry::ConfigNotFound)
};
config
}
pub async fn fetch() -> Config {
// Initialization has been ensured
let lock = ASYNC_CONFIG.get().unwrap();
lock.read().await.clone()
}
pub fn fetch_blocking() -> Config {
// Initialization has been ensured
let lock = SYNC_CONFIG.get().unwrap();
// In extreme cases, a serious error occurs in the system
lock.read().unwrap().clone()
}
pub async fn update(config: Config) {
// Initialization has been ensured
let lock = SYNC_CONFIG.get().unwrap();
// There is no lock acquired multiple times, so this is safe
*lock.write().unwrap() = config.clone();
// Initialization has been ensured
let lock = ASYNC_CONFIG.get().unwrap();
*lock.write().await = config;
fn load_config_file() -> Result<Config, Error> {
let toml_string =
fs::read_to_string("./config.toml").map_err(|err| SystemError::ConfigNotFound(err))?;
let config = toml::from_str::<ConfigTable>(&toml_string)
.map_err(|err| SystemError::InvalidConfig(err))?
.config;
Ok(config)
}
}
impl Deref for AppConfig {
type Target = Config;
fn deref(&self) -> &Self::Target {
&self.config
}
}

689
src/core/backup_engine.rs Normal file
View File

@ -0,0 +1,689 @@
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::interface::file_system::FileSystemTrait;
use crate::interface::service_unit::ServiceUnit;
use crate::model::backup::backup_execution::*;
use crate::model::error::Error;
use crate::model::error::system::SystemError;
use crate::model::error::task::TaskError;
use crate::model::event::execution::*;
use async_trait::async_trait;
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::sync::Arc;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tracing::error;
use uuid::Uuid;
pub struct BackupEngine {
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
io_manager: Arc<IOManager>,
progress_tracker: Arc<ProgressTracker>,
executions: Arc<DashMap<Uuid, BackupExecution>>,
running_executions: Arc<DashMap<Uuid, (oneshot::Sender<()>, JoinHandle<()>)>>,
}
impl BackupEngine {
pub async fn new(
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
io_manager: Arc<IOManager>,
progress_tracker: Arc<ProgressTracker>,
) -> Self {
Self {
config,
event_bus,
io_manager,
progress_tracker,
executions: Arc::new(DashMap::new()),
running_executions: Arc::new(DashMap::new()),
}
}
pub async fn stop_all_executions(&self) {
let keys: Vec<Uuid> = self
.running_executions
.iter()
.map(|pair| pair.key().clone())
.collect();
for uuid in keys {
if let Some((_, (shutdown, handle))) = self.running_executions.remove(&uuid) {
if shutdown.send(()).is_err() {
log!(SystemError::ShutdownSignalFailed);
continue;
}
if let Err(err) = handle.await {
log!(SystemError::ThreadPanic(err));
}
}
}
}
pub async fn add_execution(&self, execution: BackupExecution) {
self.executions.insert(execution.uuid, execution);
}
pub async fn remove_execution(&self, uuid: &Uuid) {
self.executions.remove(uuid);
}
pub async fn start_execution(&self, uuid: Uuid) -> Result<(), Error> {
if self.running_executions.contains_key(&uuid) {
Err(TaskError::IllegalRunState)?
}
let mut ref_mut = self
.executions
.get_mut(&uuid)
.ok_or(TaskError::ExecutionNotFound)?;
let execution = ref_mut.value_mut();
if execution.state != BackupState::Pending {
Err(TaskError::IllegalRunState)?
}
execution.state = BackupState::Running;
let execution_runner = self.to_execution_runner();
let execution = execution.clone();
let (tx, rx) = oneshot::channel();
let handle = tokio::spawn(async move { execution_runner.run(execution, rx, false).await });
self.running_executions.insert(uuid, (tx, handle));
Ok(())
}
pub async fn suspend_execution(&self, uuid: Uuid) -> Result<(), Error> {
let mut ref_mut = self
.executions
.get_mut(&uuid)
.ok_or(TaskError::ExecutionNotFound)?;
let execution = ref_mut.value_mut();
if execution.state != BackupState::Running {
Err(TaskError::IllegalRunState)?
}
execution.state = BackupState::Suspended;
drop(ref_mut);
let (_, (shutdown, handle)) = self
.running_executions
.remove(&uuid)
.ok_or(TaskError::ExecutionNotFound)?;
shutdown
.send(())
.map_err(|_| SystemError::ShutdownSignalFailed)?;
handle.await.map_err(|err| SystemError::ThreadPanic(err))?;
Ok(())
}
pub async fn resume_execution(&self, uuid: Uuid) -> Result<(), Error> {
if self.running_executions.contains_key(&uuid) {
Err(TaskError::IllegalRunState)?
}
let mut ref_mut = self
.executions
.get_mut(&uuid)
.ok_or(TaskError::ExecutionNotFound)?;
let execution = ref_mut.value_mut();
if execution.state != BackupState::Suspended {
Err(TaskError::IllegalRunState)?
}
execution.state = BackupState::Running;
let execution_runner = self.to_execution_runner();
let execution = execution.clone();
let (tx, rx) = oneshot::channel();
let handle = tokio::spawn(async move { execution_runner.run(execution, rx, true).await });
self.running_executions.insert(uuid, (tx, handle));
Ok(())
}
fn to_execution_runner(&self) -> ExecutionRunner {
let config = self.config.clone();
let io_manager = self.io_manager.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,
progress_tracker,
executions,
running_executions,
)
}
}
struct ExecutionRunner {
config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
progress_tracker: Arc<ProgressTracker>,
executions: Arc<DashMap<Uuid, BackupExecution>>,
running_executions: Arc<DashMap<Uuid, (oneshot::Sender<()>, JoinHandle<()>)>>,
}
impl ExecutionRunner {
pub fn new(
config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
progress_tracker: Arc<ProgressTracker>,
executions: Arc<DashMap<Uuid, BackupExecution>>,
running_executions: Arc<DashMap<Uuid, (oneshot::Sender<()>, JoinHandle<()>)>>,
) -> Self {
Self {
config,
io_manager,
progress_tracker,
executions,
running_executions,
}
}
async fn run(
&self,
execution: BackupExecution,
mut shutdown: oneshot::Receiver<()>,
resume: bool,
) {
let config = &self.config;
let progress_tracker = &self.progress_tracker;
let (mut current_level, mut errors) = if resume {
progress_tracker.resume_execution(execution.uuid).await
} else {
let source_root = execution.source_path.clone();
(vec![source_root], Vec::new())
};
let mut shutdown_flag = false;
while !current_level.is_empty() {
let global_queue = Arc::new(SegQueue::new());
for dir in current_level.clone() {
global_queue.push(dir);
}
let mut worker_handles = Vec::new();
let mut worker_shutdowns = Vec::new();
for _ in 0..config.max_concurrency {
let worker = self.to_worker();
let (tx, rx) = oneshot::channel();
let execution = execution.clone();
let queue = global_queue.clone();
let handle = tokio::spawn(async move { worker.run(execution, queue, rx).await });
worker_shutdowns.push(tx);
worker_handles.push(handle);
}
let workers_results = tokio::select! {
results = join_all(&mut worker_handles) => results,
_ = &mut shutdown => {
shutdown_flag = true;
for shutdown in worker_shutdowns {
if shutdown.send(()).is_err() {
log!(SystemError::ShutdownSignalFailed);
}
}
join_all(&mut worker_handles).await
}
};
let mut next_level = Vec::new();
for result in workers_results {
match result {
Ok((worker_next_level, worker_errors)) => {
next_level.extend(worker_next_level);
errors.extend(worker_errors);
}
Err(err) => log!(SystemError::ThreadPanic(err)),
}
}
if shutdown_flag {
current_level.extend(next_level);
if let Err(err) = progress_tracker
.save_execution(execution.uuid, current_level, errors)
.await {
error!("{}", err);
}
break;
} else {
current_level = next_level;
}
}
self.running_executions.remove(&execution.uuid);
match self.executions.get_mut(&execution.uuid) {
Some(mut ref_mut) => {
let execution = ref_mut.value_mut();
if shutdown_flag {
execution.state = BackupState::Suspended;
} else {
execution.state = BackupState::Completed;
}
}
None => log!(TaskError::ExecutionNotFound),
}
}
fn to_worker(&self) -> Worker {
let config = self.config.clone();
let io_manager = self.io_manager.clone();
let progress_tracker = self.progress_tracker.clone();
Worker::new(config, io_manager, progress_tracker)
}
}
struct Worker {
config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
progress_tracker: Arc<ProgressTracker>,
}
impl Worker {
pub fn new(
config: Arc<AppConfig>,
io_manager: Arc<IOManager>,
progress_tracker: Arc<ProgressTracker>,
) -> Self {
Self {
config,
io_manager,
progress_tracker,
}
}
async fn run(
&self,
execution: BackupExecution,
global_queue: Arc<SegQueue<PathBuf>>,
mut shutdown: oneshot::Receiver<()>,
) -> (Vec<PathBuf>, Vec<Error>) {
let io_manager = &self.io_manager;
let mirror = execution.options.mirror;
let mut next_level = Vec::new();
let mut errors = Vec::new();
while let Some(current_dir) = global_queue.pop() {
if shutdown.try_recv().is_ok() {
break;
}
let entries = match io_manager.list_directory(&current_dir).await {
Ok(entries) => entries,
Err(e) => {
errors.push(e);
continue;
}
};
for entry in entries.iter() {
if shutdown.try_recv().is_ok() {
break;
}
match self.process_entry(&execution, entry).await {
Ok(Some(path)) => next_level.push(path),
Ok(None) => {}
Err(e) => errors.push(e),
}
}
if mirror {
let source_entries = entries;
let destination_dir = match self.calculate_destination_path(
&current_dir,
&execution.source_path,
&execution.destination_path,
) {
Ok(dir) => dir,
Err(e) => {
errors.push(e);
continue;
}
};
match io_manager.list_directory(&destination_dir).await {
Ok(destination_entries) => {
let (_, mirror_errors) = self
.mirror_cleanup(source_entries, destination_entries)
.await;
errors.extend(mirror_errors);
}
Err(e) => errors.push(e),
}
}
}
(next_level, errors)
}
async fn process_entry(
&self,
execution: &BackupExecution,
current_path: &PathBuf,
) -> 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 is_symlink = io_manager.is_symlink(&source_path).await.unwrap_or(false);
if is_symlink {
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)
.await
} else {
self.backup_file(execution, &source_path, &destination_path)
.await
}
}
async fn backup_directory(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> Result<Option<PathBuf>, Error> {
let io_manager = &self.io_manager;
if !destination_path.exists() {
io_manager.create_directory(&destination_path).await?;
}
io_manager
.copy_attributes(source_path, destination_path)
.await?;
if execution.options.backup_permission {
io_manager
.copy_permission(source_path, destination_path)
.await?;
}
Ok(Some(source_path.clone()))
}
async fn backup_file(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> 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 => {
let comparison_mode = execution.comparison_mode.ok_or(SystemError::UnknownError)?;
self.incremental_backup(source_path, destination_path, comparison_mode)
.await?
}
}
io_manager
.copy_attributes(source_path, destination_path)
.await?;
if execution.options.backup_permission {
io_manager
.copy_permission(source_path, destination_path)
.await?;
}
drop(file_lock);
Ok(None)
}
#[inline(always)]
async fn process_symlink(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> Result<(), Error> {
if execution.options.follow_symlinks {
self.follow_symlink(execution, source_path, destination_path)
.await
} else {
self.copy_symlink(execution, source_path, destination_path)
.await
}
}
async fn follow_symlink(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> 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()));
while let Some((current_source, current_dest)) = queue.pop_front() {
let is_symlink = io_manager
.is_symlink(&current_source)
.await
.unwrap_or(false);
let canonical_path = if is_symlink {
match current_source.canonicalize() {
Ok(path) => path,
Err(_) => continue,
}
} else {
current_source.clone()
};
if visited.contains(&canonical_path) {
continue;
}
visited.insert(canonical_path.clone());
if canonical_path.is_dir() {
self.backup_directory(execution, &canonical_path, &current_dest)
.await?;
let entries = io_manager.list_directory(&canonical_path).await?;
for entry in entries {
let relative_path = match entry.strip_prefix(&canonical_path) {
Ok(rel_path) => rel_path.to_path_buf(),
Err(_) => match entry.file_name() {
Some(name) => PathBuf::from(name),
None => continue,
},
};
let new_destination = current_dest.join(relative_path);
queue.push_back((entry, new_destination));
}
} else {
self.backup_file(execution, &canonical_path, &current_dest)
.await?;
}
}
Ok(())
}
async fn copy_symlink(
&self,
execution: &BackupExecution,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> Result<(), Error> {
let io_manager = &self.io_manager;
io_manager
.copy_symlink(source_path, destination_path)
.await?;
io_manager
.copy_attributes(source_path, destination_path)
.await?;
if execution.options.backup_permission {
io_manager
.copy_permission(source_path, destination_path)
.await?;
}
Ok(())
}
#[inline(always)]
async fn full_backup(
&self,
source_path: &PathBuf,
destination_path: &PathBuf,
) -> 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,
comparison_mode: ComparisonMode,
) -> Result<(), Error> {
let io_manager = &self.io_manager;
let need_copy = !match comparison_mode {
ComparisonMode::Standard => {
io_manager
.standard_compare(source_path, destination_path)
.await
}
ComparisonMode::Advanced => {
io_manager
.advance_compare(source_path, destination_path)
.await
}
ComparisonMode::Thorough(hash_type) => {
io_manager
.thorough_compare(source_path, destination_path, hash_type)
.await
}
}?;
if need_copy {
io_manager.copy_file(source_path, destination_path).await
} else {
Ok(())
}
}
async fn mirror_cleanup(
&self,
source_entries: Vec<PathBuf>,
destination_entries: Vec<PathBuf>,
) -> ((), Vec<Error>) {
let io_manager = &self.io_manager;
let mut errors = Vec::new();
let source_names: HashSet<_> = source_entries
.into_iter()
.filter_map(|path| path.file_name().map(|name| name.to_owned()))
.collect();
for dest_entry in destination_entries {
if let Some(file_name) = dest_entry.file_name() {
if !source_names.contains(file_name) {
if dest_entry.is_dir() {
if let Err(e) = io_manager.delete_directory(&dest_entry).await {
errors.push(e);
}
} else {
if let Err(e) = io_manager.delete_file(&dest_entry).await {
errors.push(e);
}
}
}
}
}
((), errors)
}
fn calculate_destination_path(
&self,
source_path: &PathBuf,
source_root: &PathBuf,
destination_root: &PathBuf,
) -> Result<PathBuf, Error> {
let relative_path = source_path
.strip_prefix(source_root)
.map_err(|err| SystemError::UnexpectError(err))?;
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

@ -1,62 +1,65 @@
use crate::interface::database_ops::DatabaseOpsTrait;
use crate::interface::repository::schedule::ScheduleRepository;
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
use crate::model::log::database::DatabaseLog;
use crate::model::log::system::SystemLog;
use crate::platform::constants::*;
use crate::platform::database_ops::DatabaseOps;
use crate::utils::log_entry::database::DatabaseEntry;
use crate::utils::log_entry::system::SystemEntry;
use macros::log;
use sqlx::SqlitePool;
use std::ops::Deref;
use std::sync::OnceLock;
use tracing::{info, trace};
static DATABASE_MANAGER: OnceLock<DatabaseManager> = OnceLock::new();
use tokio::fs;
use tokio::fs::File;
#[derive(Debug)]
pub struct DatabaseManager {
ops: DatabaseOps,
pool: SqlitePool,
}
impl DatabaseManager {
pub async fn initialization() {
info!("{}", SystemEntry::Initializing);
DatabaseOps::lock_database().await.unwrap();
if !DatabaseOps::exist_database().await {
DatabaseOps::create_database().await.unwrap();
pub async fn new() -> Result<Self, Error> {
log!(SystemLog::Initializing);
if !Self::exist_database().await {
Self::create_database().await?;
}
let instance = match SqlitePool::connect(DATABASE_URL).await {
Ok(pool) => {
info!("{}", DatabaseEntry::DatabaseConnectSuccess);
DatabaseManager {
ops: DatabaseOps::new(pool),
}
}
Err(err) => {
trace!(?err);
panic!("{}", DatabaseEntry::DatabaseConnectFailed);
}
};
if !instance.exist_table("BackupTasks").await {
instance.create_backup_task_table().await.unwrap();
let pool = SqlitePool::connect(DATABASE_URL)
.await
.map_err(|err| DatabaseError::DatabaseConnectFailed(err))?;
log!(DatabaseLog::DatabaseConnectSuccess);
let database_manager = Self { pool };
if !database_manager.exist_table("BackupSchedules").await {
database_manager.create_backup_schedule_table().await?;
}
DATABASE_MANAGER.set(instance).unwrap();
info!("{}", SystemEntry::InitializeComplete);
log!(SystemLog::InitializeComplete);
Ok(database_manager)
}
pub fn instance() -> &'static DatabaseManager {
// Initialization has been ensured
DATABASE_MANAGER.get().unwrap()
pub fn get_pool(&self) -> SqlitePool {
self.pool.clone()
}
pub async fn terminate() {
let instance = DatabaseManager::instance();
instance.close_connection().await;
let _ = DatabaseOps::unlock_database().await;
}
}
impl Deref for DatabaseManager {
type Target = DatabaseOps;
fn deref(&self) -> &Self::Target {
&self.ops
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()
}
pub async fn create_database() -> Result<(), Error> {
let _ = File::create(DATABASE_PATH)
.await
.map_err(|err| DatabaseError::CreateDatabaseFailed(err))?;
Ok(())
}
pub async fn exist_table(&self, table_name: &str) -> bool {
let pool = self.get_pool();
sqlx::query_scalar::<_, bool>(
"SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?)",
)
.bind(table_name)
.fetch_one(&pool)
.await
.unwrap_or(false)
}
}

View File

@ -1,83 +0,0 @@
use crate::model::task::BackupTask;
use crate::utils::log_entry::task::TaskEntry;
use dashmap::DashMap;
use std::sync::OnceLock;
use tokio::sync::oneshot;
use tokio::sync::oneshot::{Receiver as OneShotReceiver, Sender as OneShotSender};
use uuid::Uuid;
pub static ENGINE: OnceLock<Engine> = OnceLock::new();
#[derive(Debug)]
pub struct Engine {
tasks: DashMap<Uuid, BackupTask>,
shutdown: DashMap<Uuid, OneShotSender<()>>,
}
impl Engine {
pub async fn initialize() {
let instance = Engine {
tasks: DashMap::new(),
shutdown: DashMap::new(),
};
ENGINE.set(instance).unwrap();
}
pub async fn instance() -> &'static Engine {
ENGINE.get().unwrap()
}
pub async fn terminate() {
let instance = Self::instance().await;
let keys: Vec<Uuid> = instance.shutdown.iter().map(|pair| pair.key().clone()).collect();
for uuid in keys {
if let Some((_, sender)) = instance.shutdown.remove(&uuid) {
let _ = sender.send(());
}
}
}
pub async fn add_task(task: BackupTask) {
let instance = Self::instance().await;
instance.tasks.insert(task.uuid, task);
}
pub async fn remove_task(task: &Uuid) {
let instance = Self::instance().await;
instance.tasks.remove(task);
}
pub async fn start_task(uuid: Uuid) -> anyhow::Result<()> {
let instance = Self::instance().await;
let task = instance
.tasks
.get(&uuid)
.ok_or(TaskEntry::TaskNotFound)?
.value()
.clone();
let (tx, rx) = oneshot::channel();
instance.shutdown.insert(uuid, tx);
tokio::spawn(async move { Engine::run_without_resume(task, rx).await });
Ok(())
}
async fn run_without_resume(task: BackupTask, shutdown: OneShotReceiver<()>) {
}
pub async fn suspend_task(uuid: Uuid) -> anyhow::Result<()> {
let instance = Self::instance().await;
let (_, channel) = instance
.shutdown
.remove(&uuid)
.ok_or(TaskEntry::TaskNotFound)?;
let _ = channel.send(());
Ok(())
}
pub async fn resume_task(uuid: Uuid, shutdown: OneShotSender<()>) {
}
async fn run_with_resume(task: BackupTask) {}
}

43
src/core/event_bus.rs Normal file
View File

@ -0,0 +1,43 @@
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_insert_with(Vec::new)
.push(handler);
rx
}
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

@ -1,25 +0,0 @@
use crate::core::event_system::actor_ref::ActorRef;
use crate::interface::event_system::actor::Actor;
use crate::interface::event_system::dispatcher::Dispatcher;
use crate::interface::event_system::event::Event;
use crate::interface::event_system::event_handler::EventHandler;
use async_trait::async_trait;
pub struct ActorDispatcher<A: Actor, E: Event> {
actor: ActorRef<A>,
handler: Box<dyn EventHandler<A, E> + Send + Sync + 'static>,
}
impl<A: Actor, E: Event> ActorDispatcher<A, E> {
pub fn new(actor: ActorRef<A>, handler: Box<dyn EventHandler<A, E> + Send + Sync + 'static>) -> Self {
Self { actor, handler }
}
}
#[async_trait]
impl<A: Actor, E: Event> Dispatcher<E> for ActorDispatcher<A, E> {
async fn dispatch(&self, event: E) {
let mut actor = self.actor.lock().await;
self.handler.handle(&mut actor, event).await;
}
}

View File

@ -1,21 +0,0 @@
use crate::interface::event_system::actor::Actor;
use std::sync::Arc;
use tokio::sync::{Mutex, MutexGuard};
pub struct ActorRef<A: Actor>(Arc<Mutex<A>>);
impl<A: Actor> ActorRef<A> {
pub fn new(actor: A) -> Self {
Self(Arc::new(Mutex::new(actor)))
}
pub async fn lock(&self) -> MutexGuard<'_, A> {
self.0.lock().await
}
}
impl<A: Actor> Clone for ActorRef<A> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

View File

@ -1,58 +0,0 @@
use crate::core::event_system::actor_ref::ActorRef;
use crate::core::event_system::listener_group::ListenerGroup;
use crate::interface::event_system::actor::Actor;
use crate::interface::event_system::event::Event;
use crate::interface::event_system::event_handler::EventHandler;
use crate::utils::log_entry::system::SystemEntry;
use dashmap::DashMap;
use std::any::{Any, TypeId};
use std::sync::OnceLock;
static EVENT_BUS: OnceLock<EventBus> = OnceLock::new();
pub struct EventBus {
listeners: DashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>,
}
impl EventBus {
fn new() -> Self {
Self {
listeners: DashMap::new(),
}
}
fn instance() -> &'static EventBus {
&EVENT_BUS.get_or_init(|| EventBus::new())
}
pub async fn subscribe<A: Actor, E: Event>(
actor: &ActorRef<A>,
handler: impl EventHandler<A, E> + Send + Sync + 'static,
) -> anyhow::Result<()> {
let instance = Self::instance();
let type_id = TypeId::of::<ListenerGroup<E>>();
let mut entry = instance
.listeners
.entry(type_id)
.or_insert_with(|| Box::new(ListenerGroup::<E>::new()));
let listeners = entry
.value_mut()
.downcast_mut::<ListenerGroup<E>>()
.ok_or(SystemEntry::InternalError)?;
listeners.subscribe(actor.clone(), handler);
Ok(())
}
pub async fn publish<E: Event>(event: E) -> anyhow::Result<()> {
let instance = Self::instance();
let type_id = TypeId::of::<ListenerGroup<E>>();
if let Some(listeners) = instance.listeners.get_mut(&type_id) {
let listeners = listeners
.value()
.downcast_ref::<ListenerGroup<E>>()
.ok_or(SystemEntry::InternalError)?;
listeners.broadcast(event).await;
}
Ok(())
}
}

View File

@ -1,37 +0,0 @@
use crate::core::event_system::actor_dispatcher::ActorDispatcher;
use crate::core::event_system::actor_ref::ActorRef;
use crate::interface::event_system::actor::Actor;
use crate::interface::event_system::dispatcher::Dispatcher;
use crate::interface::event_system::event::Event;
use crate::interface::event_system::event_handler::EventHandler;
use futures::future;
pub struct ListenerGroup<E: Event> {
dispatchers: Vec<Box<dyn Dispatcher<E> + Send + Sync + 'static>>,
}
impl<E: Event> ListenerGroup<E> {
pub fn new() -> Self {
ListenerGroup {
dispatchers: Vec::new(),
}
}
pub fn subscribe<A: Actor>(
&mut self,
actor: ActorRef<A>,
handler: impl EventHandler<A, E> + Send + Sync + 'static,
) {
let handler = Box::new(handler);
let actor_dispatcher = ActorDispatcher::new(actor, handler);
self.dispatchers.push(Box::new(actor_dispatcher));
}
pub async fn broadcast(&self, event: E) {
let futures = self
.dispatchers
.iter()
.map(|dispatcher| dispatcher.dispatch(event.clone()));
future::join_all(futures).await;
}
}

View File

@ -1,4 +0,0 @@
pub mod actor_dispatcher;
pub mod actor_ref;
pub mod event_bus;
pub mod listener_group;

62
src/core/gui_manager.rs Normal file
View File

@ -0,0 +1,62 @@
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 eframe::egui;
use std::sync::Arc;
use crate::core::app_config::AppConfig;
pub struct GuiManager {
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
schedule_manager: Arc<ScheduleManager>,
}
impl GuiManager {
pub fn new(
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
schedule_manager: Arc<ScheduleManager>,
) -> Self {
Self {
config,
event_bus,
backup_engine,
schedule_manager,
}
}
pub fn start(&self) -> Result<(), Error> {
let config = self.config.clone();
let event_bus = self.event_bus.clone();
let backup_engine = self.backup_engine.clone();
let schedule_manager = self.schedule_manager.clone();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1200.0, 800.0])
.with_title("MirrorSphere"),
..Default::default()
};
eframe::run_native(
"MirrorSphere",
options,
Box::new(|_| {
Ok(Box::new(MainPage::new(
config,
event_bus,
backup_engine,
schedule_manager,
)))
}),
)
.map_err(|err| MiscError::UIPlatformError(err))?;
Ok(())
}
}

View File

@ -2,34 +2,24 @@ use crate::core::app_config::AppConfig;
use crate::interface::file_system::FileSystemTrait;
use crate::platform::file_system::FileSystem;
use std::ops::Deref;
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use tokio::sync::Semaphore;
pub static IO_MANAGER: OnceLock<IOManager> = OnceLock::new();
pub struct IOManager {
file_system: FileSystem,
}
impl IOManager {
pub async fn initialize() {
let config = AppConfig::fetch().await;
pub fn new(config: Arc<AppConfig>) -> Self {
let max_file_operations = config.max_file_operations;
let semaphore = Arc::new(Semaphore::new(max_file_operations));
let instance = IOManager {
Self {
file_system: FileSystem::new(semaphore),
};
IO_MANAGER.get_or_init(|| instance);
}
}
pub fn instance() -> &'static IOManager {
// Initialization has been ensured
IO_MANAGER.get().unwrap()
}
pub fn terminate() {
let instance = Self::instance();
instance.file_system.semaphore().close();
pub fn terminate(&self) {
self.file_system.semaphore().close();
}
}

View File

@ -1,7 +1,9 @@
pub mod app_config;
pub mod backup_engine;
pub mod database_manager;
pub mod engine;
pub mod event_system;
pub mod event_bus;
pub mod gui_manager;
pub mod io_manager;
pub mod progress_tracker;
pub mod schedule_manager;
pub mod system;

View File

@ -1,7 +1,106 @@
use crate::interface::event_system::actor::Actor;
use crate::core::io_manager::IOManager;
use crate::interface::file_system::FileSystemTrait;
use crate::model::backup::progress_data::ProgressData;
use crate::model::error::Error;
use crate::model::error::io::IOError;
use crate::model::error::misc::MiscError;
use crate::platform::constants::PROGRESS_SAVE_PATH;
use memmap2::MmapMut;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs::OpenOptions;
use uuid::Uuid;
pub struct ProgressTracker {}
pub struct ProgressTracker {
io_manager: Arc<IOManager>,
}
impl ProgressTracker {}
impl ProgressTracker {
pub fn new(io_manager: Arc<IOManager>) -> Self {
Self { io_manager }
}
impl Actor for ProgressTracker {}
pub async fn save_execution(
&self,
execution_uuid: Uuid,
current_level: Vec<PathBuf>,
errors: Vec<Error>,
) -> Result<(), Error> {
let progress_data = ProgressData::new(current_level, errors);
self.write_progress_file(execution_uuid, &progress_data)
.await
}
pub async fn resume_execution(&self, execution_uuid: Uuid) -> (Vec<PathBuf>, Vec<Error>) {
match self.read_progress_file(execution_uuid).await {
Ok(progress_data) => (progress_data.current_level, progress_data.errors),
Err(_) => (Vec::new(), Vec::new()),
}
}
async fn write_progress_file(
&self,
execution_uuid: Uuid,
data: &ProgressData,
) -> Result<(), Error> {
let saved_path = PathBuf::from(PROGRESS_SAVE_PATH).join(execution_uuid.to_string());
if let Some(parent) = saved_path.parent() {
let instance = &self.io_manager;
let parent = parent.to_path_buf();
instance.create_directory(&parent).await?;
}
let config = bincode::config::standard();
let serialized = bincode::serde::encode_to_vec(data, config)
.map_err(|err| MiscError::DeserializeError(err))?;
let data_len = serialized.len();
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&saved_path)
.await
.map_err(|err| IOError::CreateFileFailed(saved_path.clone(), err))?;
file.set_len(data_len as u64)
.await
.map_err(|err| IOError::WriteFileFailed(saved_path.clone(), err))?;
let mut mmap = unsafe {
MmapMut::map_mut(&file)
.map_err(|err| IOError::WriteFileFailed(saved_path.clone(), err))?
};
mmap[..data_len].copy_from_slice(&serialized);
mmap.flush()
.map_err(|err| IOError::WriteFileFailed(saved_path, err))?;
Ok(())
}
async fn read_progress_file(&self, execution_uuid: Uuid) -> Result<ProgressData, Error> {
let saved_path = PathBuf::from(PROGRESS_SAVE_PATH).join(execution_uuid.to_string());
if !saved_path.exists() {
Err(IOError::FileDoesNotExist {
path: saved_path.clone(),
})?
}
let file = tokio::fs::File::open(&saved_path)
.await
.map_err(|err| IOError::ReadFileFailed(saved_path.clone(), err))?;
let mmap = unsafe {
MmapMut::map_mut(&file).map_err(|err| IOError::ReadFileFailed(saved_path, err))?
};
let config = bincode::config::standard();
let (progress_data, _) = bincode::serde::decode_from_slice(&mmap, config)
.map_err(|err| MiscError::DeserializeError(err))?;
Ok(progress_data)
}
}

View File

@ -0,0 +1,308 @@
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 {
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
database_manager: Arc<DatabaseManager>,
}
impl ScheduleManager {
pub fn new(
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
database_manager: Arc<DatabaseManager>,
) -> Self {
ScheduleManager {
config,
event_bus,
database_manager,
}
}
pub async fn get_schedule(&self, uuid: Uuid) -> Result<Option<BackupSchedule>, Error> {
self.database_manager.get_backup_schedule(uuid).await
}
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 {
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
schedule_manager: Arc<ScheduleManager>,
shutdown_rx: Option<oneshot::Receiver<()>>,
refresh_rx: mpsc::UnboundedReceiver<()>,
}
impl ScheduleTimer {
pub fn new(
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
schedule_manager: Arc<ScheduleManager>,
shutdown_rx: oneshot::Receiver<()>,
refresh_rx: mpsc::UnboundedReceiver<()>,
) -> Self {
ScheduleTimer {
config,
event_bus,
schedule_manager,
shutdown_rx: Some(shutdown_rx),
refresh_rx,
}
}
pub async fn run(mut self) {
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.config.default_wakeup_time),
Err(err) => {
error!("{}", err);
Duration::seconds(self.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()) => {}
}
},
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.config.clone(),
self.event_bus.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.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,40 +1,113 @@
use crate::core::app_config::AppConfig;
use crate::core::backup_engine::BackupEngine;
use crate::core::database_manager::DatabaseManager;
use crate::core::engine::Engine;
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::model::error::Error;
use crate::model::error::system::SystemError;
use crate::model::log::system::SystemLog;
#[cfg(not(debug_assertions))]
use crate::platform::elevate::elevate;
use crate::utils::log_entry::system::SystemEntry;
use crate::utils::database_lock::DatabaseLock;
use crate::utils::logging::Logging;
use macros::log;
#[cfg(not(debug_assertions))]
use privilege::user::privileged;
use tracing::info;
use std::mem;
#[cfg(not(debug_assertions))]
use std::process;
use std::sync::Arc;
use tokio::sync::oneshot;
pub struct System;
pub struct System {
pub event_bus: Arc<EventBus>,
pub app_config: Arc<AppConfig>,
pub io_manager: Arc<IOManager>,
pub progress_tracker: Arc<ProgressTracker>,
pub database_lock: DatabaseLock,
pub database_manager: Arc<DatabaseManager>,
pub gui_manager: Arc<GuiManager>,
pub schedule_manager: Arc<ScheduleManager>,
pub backup_engine: Arc<BackupEngine>,
pub shutdowns: Vec<oneshot::Sender<()>>,
}
impl System {
pub async fn initialize() {
pub async fn new() -> Result<Self, Error> {
Logging::initialize().await;
info!("{}", SystemEntry::Initializing);
log!(SystemLog::Initializing);
#[cfg(not(debug_assertions))]
if !privileged() {
info!("{}", SystemEntry::ReRunAsAdmin);
elevate()
.map_err(|_| SystemEntry::RunAsAdminFailed)
.unwrap();
log!(SystemLog::ReRunAsAdmin);
elevate()?;
process::exit(0);
}
AppConfig::initialization().await;
Engine::initialize().await;
IOManager::initialize().await;
DatabaseManager::initialization().await;
info!("{}", SystemEntry::InitializeComplete);
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 progress_tracker = Arc::new(ProgressTracker::new(io_manager.clone()));
let database_lock = DatabaseLock::acquire().await?;
let database_manager = Arc::new(DatabaseManager::new().await?);
let schedule_manager = Arc::new(ScheduleManager::new(
app_config.clone(),
event_bus.clone(),
database_manager.clone(),
));
let backup_engine = Arc::new(
BackupEngine::new(
app_config.clone(),
event_bus.clone(),
io_manager.clone(),
progress_tracker.clone(),
)
.await,
);
let gui_manager = Arc::new(GuiManager::new(
app_config.clone(),
event_bus.clone(),
backup_engine.clone(),
schedule_manager.clone(),
));
log!(SystemLog::InitializeComplete);
Ok(Self {
event_bus,
app_config,
io_manager,
progress_tracker,
database_lock,
database_manager,
gui_manager,
schedule_manager,
backup_engine,
shutdowns: Vec::new(),
})
}
pub async fn run() {
info!("{}", SystemEntry::Online);
pub async fn run(&mut self) -> Result<(), Error> {
let gui_manager = self.gui_manager.clone();
let schedule_manager_shutdown = self.schedule_manager.clone().run().await;
let backup_engine_shutdown = self.backup_engine.clone().run().await;
self.shutdowns.push(schedule_manager_shutdown);
self.shutdowns.push(backup_engine_shutdown);
gui_manager.start()
}
pub async fn terminate() {
info!("{}", SystemEntry::Terminating);
Engine::terminate().await;
DatabaseManager::terminate().await;
info!("{}", SystemEntry::TerminateComplete);
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);
}
}

View File

@ -1,171 +0,0 @@
use crate::model::task::BackupTask;
use crate::platform::constants::{DATABASE_LOCK_PATH, DATABASE_PATH};
use crate::utils::log_entry::database::DatabaseEntry;
use async_trait::async_trait;
use sqlx::SqlitePool;
use tokio::fs;
use tokio::fs::File;
use uuid::Uuid;
#[async_trait]
pub trait DatabaseOpsTrait {
fn new(pool: SqlitePool) -> Self;
fn get_pool(&self) -> SqlitePool;
async fn exist_database() -> bool {
fs::metadata(DATABASE_PATH).await.is_ok()
}
async fn create_database() -> anyhow::Result<()> {
let _ = File::create(DATABASE_PATH)
.await
.map_err(|_| DatabaseEntry::CreateDatabaseFailed)?;
Ok(())
}
async fn lock_database() -> anyhow::Result<()> {
if fs::metadata(DATABASE_LOCK_PATH).await.is_err() {
File::create(&DATABASE_LOCK_PATH)
.await
.map_err(|_| DatabaseEntry::LockDatabaseFailed)?;
Ok(())
} else {
Err(DatabaseEntry::LockDatabaseFailed)?
}
}
async fn unlock_database() -> anyhow::Result<()> {
fs::remove_file(&DATABASE_LOCK_PATH)
.await
.map_err(|_| DatabaseEntry::UnlockDatabaseFailed)?;
Ok(())
}
async fn close_connection(&self) {
let pool = self.get_pool();
pool.close().await
}
async fn exist_table(&self, table_name: &str) -> bool {
let pool = self.get_pool();
sqlx::query_scalar::<_, bool>(
"SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?)",
)
.bind(table_name)
.fetch_one(&pool)
.await
.unwrap_or(false)
}
async fn create_backup_task_table(&self) -> anyhow::Result<()> {
let pool = self.get_pool();
sqlx::query(
r#"
CREATE TABLE BackupTasks (
uuid BLOB PRIMARY KEY,
source_path TEXT NOT NULL,
destination_path TEXT NOT NULL,
backup_type TEXT NOT NULL,
comparison_mode TEXT NOT NULL,
options TEXT NOT NULL,
schedule INTEGER NOT NULL,
last_run_time INTEGER,
next_run_time INTEGER
)
"#,
)
.execute(&pool)
.await?;
Ok(())
}
async fn add_backup_task(&self, backup_task: BackupTask) -> anyhow::Result<()> {
let pool = self.get_pool();
sqlx::query(
r#"
INSERT INTO BackupTasks (
uuid,
source_path,
destination_path,
backup_type,
comparison_mode,
options,
schedule,
last_run_time,
next_run_time
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(backup_task.uuid)
.bind(backup_task.source_path.to_string_lossy().to_string())
.bind(backup_task.destination_path.to_string_lossy().to_string())
.bind(serde_json::to_string(&backup_task.backup_type)?)
.bind(serde_json::to_string(&backup_task.comparison_mode)?)
.bind(serde_json::to_string(&backup_task.options)?)
.bind(backup_task.schedule)
.bind(
backup_task
.last_run_time
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64),
)
.bind(
backup_task
.next_run_time
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64),
)
.execute(&pool)
.await?;
Ok(())
}
async fn modify_backup_task(&self, backup_task: BackupTask) -> anyhow::Result<()> {
let pool = self.get_pool();
sqlx::query(
r#"
UPDATE BackupTasks
SET
source_path = ?,
destination_path = ?,
backup_type = ?,
comparison_mode = ?,
options = ?,
schedule = ?,
last_run_time = ?,
next_run_time = ?
WHERE uuid = ?
"#,
)
.bind(backup_task.source_path.to_string_lossy().to_string())
.bind(backup_task.destination_path.to_string_lossy().to_string())
.bind(serde_json::to_string(&backup_task.backup_type)?)
.bind(serde_json::to_string(&backup_task.comparison_mode)?)
.bind(serde_json::to_string(&backup_task.options)?)
.bind(backup_task.schedule)
.bind(
backup_task
.last_run_time
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64),
)
.bind(
backup_task
.next_run_time
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64),
)
.bind(backup_task.uuid)
.execute(&pool)
.await?;
Ok(())
}
async fn remove_backup_task(&self, uuid: Uuid) -> anyhow::Result<()> {
let pool = self.get_pool();
sqlx::query("DELETE FROM BackupTasks WHERE uuid = ?")
.bind(uuid)
.execute(&pool)
.await?;
Ok(())
}
}

View File

@ -1 +0,0 @@
pub trait Actor: Send + Sync + 'static {}

View File

@ -1,7 +0,0 @@
use crate::interface::event_system::event::Event;
use async_trait::async_trait;
#[async_trait]
pub trait Dispatcher<E: Event> {
async fn dispatch(&self, event: E);
}

View File

@ -1,19 +0,0 @@
use crate::interface::event_system::actor::Actor;
use crate::interface::event_system::event::Event;
use async_trait::async_trait;
use futures::future::BoxFuture;
#[async_trait]
pub trait EventHandler<A: Actor, E: Event> {
async fn handle(&self, actor: &mut A, event: E);
}
#[async_trait]
impl<A: Actor, E: Event, F> EventHandler<A, E> for F
where
F: for<'a> Fn(&'a mut A, E) -> BoxFuture<'a, ()> + Send + Sync + 'static,
{
async fn handle(&self, actor: &mut A, event: E) {
self(actor, event).await
}
}

View File

@ -1,4 +0,0 @@
pub mod actor;
pub mod dispatcher;
pub mod event;
pub mod event_handler;

View File

@ -1,12 +1,10 @@
use crate::core::event_system::event_bus::EventBus;
use crate::model::event::io::directory::*;
use crate::model::event::io::file::*;
use crate::model::event::io::hash::*;
use crate::model::task::HashType;
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::platform::attributes::*;
use crate::utils::file_hash::*;
use crate::utils::log_entry::io::IOEntry;
use crate::utils::log_entry::system::SystemEntry;
use crate::utils::file_lock::FileLock;
use async_trait::async_trait;
use std::path::PathBuf;
use std::sync::Arc;
@ -15,7 +13,6 @@ use tokio::sync::Semaphore;
use tokio::task::spawn_blocking;
use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt;
use uuid::Uuid;
#[async_trait]
pub trait FileSystemTrait {
@ -23,207 +20,232 @@ pub trait FileSystemTrait {
fn semaphore(&self) -> Arc<Semaphore>;
async fn list_directory(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<Vec<PathBuf>> {
async fn is_symlink(&self, path: &PathBuf) -> Result<bool, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
let mut result = Vec::new();
let reader = fs::read_dir(&path)
.map_err(|err| IOError::SemaphoreClosed(err))?;
let symlink_metadata = tokio::fs::symlink_metadata(path)
.await
.map_err(|_| IOEntry::ReadDirectoryFailed)?;
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
Ok(symlink_metadata.file_type().is_symlink())
}
async fn create_symlink(&self, target: &PathBuf, link_path: &PathBuf) -> Result<(), Error>;
async fn copy_symlink(
&self,
source_link: &PathBuf,
destination_link: &PathBuf,
) -> Result<(), Error>;
async fn list_directory(&self, path: &PathBuf) -> Result<Vec<PathBuf>, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|err| IOError::SemaphoreClosed(err))?;
let mut result = Vec::new();
let reader = fs::read_dir(path)
.await
.map_err(|err| IOError::ReadDirectoryFailed(path.clone(), err))?;
let mut entries = ReadDirStream::new(reader);
while let Some(entry) = entries.next().await {
let path = entry.map_err(|_| IOEntry::ReadFileFailed)?.path();
let path = entry
.map_err(|err| IOError::ReadDirectoryFailed(path.clone(), err))?
.path();
result.push(path);
}
let event = ListDirectoryEvent { task_id, path };
EventBus::publish(event).await?;
Ok(result)
}
async fn create_directory(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<()> {
async fn create_directory(&self, path: &PathBuf) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
fs::create_dir_all(&path)
.map_err(|err| IOError::SemaphoreClosed(err))?;
fs::create_dir_all(path)
.await
.map_err(|_| IOEntry::CreateDirectoryFailed)?;
let event = CreateDirectoryEvent { task_id, path };
EventBus::publish(event).await?;
.map_err(|err| IOError::CreateDirectoryFailed(path.clone(), err))?;
Ok(())
}
async fn delete_directory(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<()> {
async fn delete_directory(&self, path: &PathBuf) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
fs::remove_dir_all(&path)
.map_err(|err| IOError::SemaphoreClosed(err))?;
fs::remove_dir_all(path)
.await
.map_err(|_| IOEntry::DeleteDirectoryFailed)?;
let event = DeleteDirectoryEvent { task_id, path };
EventBus::publish(event).await?;
.map_err(|err| IOError::DeleteDirectoryFailed(path.clone(), err))?;
Ok(())
}
async fn copy_file(
&self,
task_id: Uuid,
source: PathBuf,
destination: PathBuf,
) -> anyhow::Result<()> {
async fn copy_file(&self, source: &PathBuf, destination: &PathBuf) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
fs::copy(&source, &destination)
.map_err(|err| IOError::SemaphoreClosed(err))?;
fs::copy(source, destination)
.await
.map_err(|_| IOEntry::CopyFileFailed)?;
let event = CopyFileEvent {
task_id,
source,
destination,
};
EventBus::publish(event).await?;
.map_err(|err| IOError::CopyFileFailed(source.clone(), destination.clone(), err))?;
Ok(())
}
async fn delete_file(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<()> {
async fn delete_file(&self, path: &PathBuf) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
fs::remove_file(&path)
.map_err(|err| IOError::SemaphoreClosed(err))?;
fs::remove_file(path)
.await
.map_err(|_| IOEntry::DeleteFileFailed)?;
let event = DeleteFileEvent { task_id, path };
EventBus::publish(event).await?;
.map_err(|err| IOError::DeleteFileFailed(path.clone(), err))?;
Ok(())
}
async fn get_attributes(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<Attributes>;
async fn get_attributes(&self, path: &PathBuf) -> Result<Attributes, Error>;
async fn set_attributes(
&self,
task_id: Uuid,
path: PathBuf,
attributes: Attributes,
) -> anyhow::Result<()>;
async fn set_attributes(&self, path: &PathBuf, attributes: Attributes) -> Result<(), Error>;
async fn copy_attributes(&self, source: &PathBuf, destination: &PathBuf) -> Result<(), Error> {
let source_attributes = self.get_attributes(source).await?;
self.set_attributes(destination, source_attributes).await?;
Ok(())
}
async fn compare_attributes(
&self,
task_id: Uuid,
source: PathBuf,
destination: PathBuf,
) -> anyhow::Result<bool>;
source: &PathBuf,
destination: &PathBuf,
) -> 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, task_id: Uuid, path: PathBuf) -> anyhow::Result<Permissions>;
async fn get_permission(&self, path: &PathBuf) -> Result<Permissions, Error>;
async fn set_permission(
&self,
task_id: Uuid,
path: PathBuf,
permission: Permissions,
) -> anyhow::Result<()>;
async fn set_permission(&self, path: &PathBuf, permissions: Permissions) -> Result<(), Error>;
async fn standard_compare(
&self,
task_id: Uuid,
source: PathBuf,
destination: PathBuf,
) -> anyhow::Result<bool> {
if !self
.compare_attributes(task_id, source.clone(), destination.clone())
.await?
{
return Ok(false);
}
async fn copy_permission(&self, source: &PathBuf, destination: &PathBuf) -> 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> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
.map_err(|err| IOError::SemaphoreClosed(err))?;
let source_metadata = fs::metadata(&source)
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(|_| IOEntry::GetMetadataFailed)?;
let destination_metadata = fs::metadata(&destination)
.map_err(|err| IOError::SemaphoreClosed(err))?;
let path_clone = path.clone();
let hash = spawn_blocking(move || {
let path = path_clone;
match hash_type {
HashType::MD5 => md5(path),
HashType::SHA3 => sha3(path),
HashType::SHA256 => sha256(path),
HashType::BLAKE2B => blake2b(path),
HashType::BLAKE2S => blake2s(path),
HashType::BLAKE3 => blake3(path),
}
})
.await
.map_err(|err| SystemError::ThreadPanic(err))??;
Ok(hash)
}
async fn standard_compare(
&self,
source: &PathBuf,
destination: &PathBuf,
) -> Result<bool, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::GetMetadataFailed)?;
.map_err(|err| IOError::SemaphoreClosed(err))?;
let source_metadata =
fs::metadata(source)
.await
.map_err(|err| IOError::GetMetadataFailed(source.clone(), err))?;
let destination_metadata =
fs::metadata(destination)
.await
.map_err(|err| IOError::GetMetadataFailed(destination.clone(), err))?;
if source_metadata.len() != destination_metadata.len() {
return Ok(false);
}
let source_modified = source_metadata
.modified()
.map_err(|_| IOEntry::GetMetadataFailed)?;
let destination_modified = destination_metadata
.modified()
.map_err(|_| IOEntry::GetMetadataFailed)?;
let source_modified =
source_metadata
.modified()
.map_err(|err| IOError::GetMetadataFailed(source.clone(), err))?;
let destination_modified =
destination_metadata
.modified()
.map_err(|err| IOError::GetMetadataFailed(destination.clone(), err))?;
if source_modified != destination_modified {
return Ok(false);
}
Ok(true)
}
async fn advance_compare(
&self,
source: &PathBuf,
destination: &PathBuf,
) -> Result<bool, Error> {
if !self.standard_compare(source, destination).await? {
return Ok(false);
}
if !self.compare_attributes(source, destination).await? {
return Ok(false);
}
Ok(true)
}
async fn thorough_compare(
&self,
task_id: Uuid,
source: PathBuf,
destination: PathBuf,
source: &PathBuf,
destination: &PathBuf,
hash_type: HashType,
) -> anyhow::Result<bool> {
if !self
.standard_compare(task_id, source.clone(), destination.clone())
.await?
{
) -> Result<bool, Error> {
if !self.advance_compare(source, destination).await? {
return Ok(false);
}
let source_file_hash = self.calculate_hash(task_id, source, hash_type).await?;
let destination_file_hash = self.calculate_hash(task_id, destination, hash_type).await?;
let source_file_hash = self.calculate_hash(source, hash_type).await?;
let destination_file_hash = self.calculate_hash(destination, hash_type).await?;
Ok(source_file_hash == destination_file_hash)
}
async fn calculate_hash(
&self,
task_id: Uuid,
path: PathBuf,
hash_type: HashType,
) -> anyhow::Result<Vec<u8>> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
let path_clone = path.clone();
let hash = spawn_blocking(move || {
let path = path_clone;
let file = std::fs::File::open(path)?;
match hash_type {
HashType::MD5 => md5(file),
HashType::SHA3 => sha3(file),
HashType::SHA256 => sha256(file),
HashType::BLAKE2B => blake2b(file),
HashType::BLAKE2S => blake2s(file),
HashType::BLAKE3 => blake3(file),
}
})
.await
.map_err(|_| SystemEntry::ThreadPanic)??;
let event = CalculateHashEvent { task_id, path };
EventBus::publish(event).await?;
Ok(hash)
}
}

View File

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

View File

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

View File

@ -0,0 +1,295 @@
use crate::core::database_manager::DatabaseManager;
use crate::model::backup::backup_schedule::BackupSchedule;
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
use crate::model::error::misc::MiscError;
use sqlx::Row;
use uuid::Uuid;
pub trait ScheduleRepository {
async fn create_backup_schedule_table(&self) -> Result<(), Error>;
async fn create_backup_schedule(&self, backup_schedule: &BackupSchedule) -> Result<(), Error>;
async fn modify_backup_schedule(&self, backup_schedule: &BackupSchedule) -> Result<(), Error>;
async fn remove_backup_schedule(&self, uuid: Uuid) -> Result<(), Error>;
async fn get_backup_schedule(&self, uuid: Uuid) -> Result<Option<BackupSchedule>, Error>;
async fn get_all_backup_schedules(&self) -> Result<Vec<BackupSchedule>, Error>;
}
impl ScheduleRepository for DatabaseManager {
async fn create_backup_schedule_table(&self) -> Result<(), Error> {
let pool = self.get_pool();
sqlx::query(
r#"
CREATE TABLE BackupSchedules (
uuid BLOB PRIMARY KEY,
name TEXT NOT NULL,
state TEXT NOT NULL,
source_path TEXT NOT NULL,
destination_path TEXT NOT NULL,
backup_type TEXT NOT NULL,
comparison_mode TEXT,
options TEXT NOT NULL,
interval TEXT NOT NULL,
last_run_time TEXT,
next_run_time TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"#,
)
.execute(&pool)
.await
.map_err(|err| DatabaseError::StatementExecutionFailed(err))?;
Ok(())
}
async fn create_backup_schedule(&self, backup_schedule: &BackupSchedule) -> Result<(), Error> {
let pool = self.get_pool();
sqlx::query(
r#"
INSERT INTO BackupSchedules (
uuid,
name,
state,
source_path,
destination_path,
backup_type,
comparison_mode,
options,
interval,
last_run_time,
next_run_time,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(backup_schedule.uuid.as_bytes().as_slice())
.bind(&backup_schedule.name)
.bind(
serde_json::to_string(&backup_schedule.state)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(backup_schedule.source_path.to_string_lossy().to_string())
.bind(backup_schedule.destination_path.to_string_lossy().to_string())
.bind(
serde_json::to_string(&backup_schedule.backup_type)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(
serde_json::to_string(&backup_schedule.comparison_mode)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(
serde_json::to_string(&backup_schedule.options)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(
serde_json::to_string(&backup_schedule.interval)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(backup_schedule.last_run_time)
.bind(backup_schedule.next_run_time)
.bind(backup_schedule.created_at)
.bind(backup_schedule.updated_at)
.execute(&pool)
.await
.map_err(|err| DatabaseError::StatementExecutionFailed(err))?;
Ok(())
}
async fn modify_backup_schedule(&self, backup_schedule: &BackupSchedule) -> Result<(), Error> {
let pool = self.get_pool();
sqlx::query(
r#"
UPDATE BackupSchedules
SET
name = ?,
state = ?,
source_path = ?,
destination_path = ?,
backup_type = ?,
comparison_mode = ?,
options = ?,
interval = ?,
last_run_time = ?,
next_run_time = ?,
created_at = ?,
updated_at = ?
WHERE uuid = ?
"#,
)
.bind(backup_schedule.uuid.as_bytes().as_slice())
.bind(&backup_schedule.name)
.bind(
serde_json::to_string(&backup_schedule.state)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(backup_schedule.source_path.to_string_lossy().to_string())
.bind(backup_schedule.destination_path.to_string_lossy().to_string())
.bind(
serde_json::to_string(&backup_schedule.backup_type)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(
serde_json::to_string(&backup_schedule.comparison_mode)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(
serde_json::to_string(&backup_schedule.options)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(
serde_json::to_string(&backup_schedule.interval)
.map_err(|err| MiscError::SerializeError(err))?,
)
.bind(backup_schedule.last_run_time)
.bind(backup_schedule.next_run_time)
.bind(backup_schedule.created_at)
.bind(backup_schedule.updated_at)
.execute(&pool)
.await
.map_err(|err| DatabaseError::StatementExecutionFailed(err))?;
Ok(())
}
async fn remove_backup_schedule(&self, uuid: Uuid) -> Result<(), Error> {
let pool = self.get_pool();
sqlx::query("DELETE FROM BackupSchedules WHERE uuid = ?")
.bind(uuid.as_bytes().as_slice())
.execute(&pool)
.await
.map_err(|err| DatabaseError::StatementExecutionFailed(err))?;
Ok(())
}
async fn get_backup_schedule(&self, uuid: Uuid) -> Result<Option<BackupSchedule>, Error> {
let pool = self.get_pool();
let row = sqlx::query(
r#"
SELECT
uuid,
name,
state,
source_path,
destination_path,
backup_type,
comparison_mode,
options,
"interval",
last_run_time,
next_run_time,
created_at,
updated_at
FROM BackupSchedules
WHERE uuid = ?
"#,
)
.bind(uuid.as_bytes().as_slice())
.fetch_optional(&pool)
.await
.map_err(|err| DatabaseError::StatementExecutionFailed(err))?;
if let Some(row) = row {
let uuid_bytes: Vec<u8> = row.get("uuid");
let uuid = Uuid::from_slice(&uuid_bytes).map_err(|_| DatabaseError::DataCorrupted)?;
let state_str: String = row.get("state");
let state = serde_json::from_str(&state_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let backup_type_str: String = row.get("backup_type");
let backup_type = serde_json::from_str(&backup_type_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let comparison_mode_str: String = row.get("comparison_mode");
let comparison_mode = serde_json::from_str(&comparison_mode_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let options_str: String = row.get("options");
let options = serde_json::from_str(&options_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let interval_str: String = row.get("interval");
let interval = serde_json::from_str(&interval_str)
.map_err(|err| MiscError::DeserializeError(err))?;
Ok(Some(BackupSchedule {
uuid,
name: row.get("name"),
state,
source_path: row.get::<String, _>("source_path").into(),
destination_path: row.get::<String, _>("destination_path").into(),
backup_type,
comparison_mode,
options,
interval,
last_run_time: row.get("last_run_time"),
next_run_time: row.get("next_run_time"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
}))
} else {
Ok(None)
}
}
async fn get_all_backup_schedules(&self) -> Result<Vec<BackupSchedule>, Error> {
let pool = self.get_pool();
let rows = sqlx::query(
r#"
SELECT uuid, name, state, source_path, destination_path, schedule_type,
last_run_time, next_run_time, created_at, updated_at
FROM BackupSchedules
"#,
)
.fetch_all(&pool)
.await
.map_err(|err| DatabaseError::StatementExecutionFailed(err))?;
let mut schedules = Vec::new();
for row in rows {
let uuid_bytes: Vec<u8> = row.get("uuid");
let uuid = Uuid::from_slice(&uuid_bytes).map_err(|_| DatabaseError::DataCorrupted)?;
let state_str: String = row.get("state");
let state = serde_json::from_str(&state_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let backup_type_str: String = row.get("backup_type");
let backup_type = serde_json::from_str(&backup_type_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let comparison_mode_str: String = row.get("comparison_mode");
let comparison_mode = serde_json::from_str(&comparison_mode_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let options_str: String = row.get("options");
let options = serde_json::from_str(&options_str)
.map_err(|err| MiscError::DeserializeError(err))?;
let interval_str: String = row.get("interval");
let interval = serde_json::from_str(&interval_str)
.map_err(|err| MiscError::DeserializeError(err))?;
schedules.push(BackupSchedule {
uuid,
name: row.get("name"),
state,
source_path: row.get::<String, _>("source_path").into(),
destination_path: row.get::<String, _>("destination_path").into(),
backup_type,
comparison_mode,
options,
interval,
last_run_time: row.get("last_run_time"),
next_run_time: row.get("next_run_time"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
});
}
Ok(schedules)
}
}

View File

@ -0,0 +1,16 @@
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

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

View File

@ -1,13 +1,15 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::SystemTime;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackupState {
Running,
Pending,
Suspended,
Stopped,
Completed,
Failed,
Canceled,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
@ -30,20 +32,22 @@ pub enum HashType {
pub enum ComparisonMode {
// Compare size and modify time
Standard,
// Standard + regular file attr + compare file checksum
// Standard + regular file attr
Advanced,
// Advanced + compare file checksum
Thorough(HashType),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct BackupOptions {
lock_source: bool,
backup_acl: bool,
backup_other_file: bool,
advanced_file_attr: bool,
pub mirror: bool,
pub lock_source: bool,
pub backup_permission: bool,
pub follow_symlinks: bool,
}
#[derive(Debug, Clone)]
pub struct BackupTask {
pub struct BackupExecution {
pub uuid: Uuid,
pub state: BackupState,
pub source_path: PathBuf,
@ -51,7 +55,4 @@ pub struct BackupTask {
pub backup_type: BackupType,
pub comparison_mode: Option<ComparisonMode>,
pub options: BackupOptions,
pub schedule: bool,
pub last_run_time: Option<SystemTime>,
pub next_run_time: Option<SystemTime>,
}

View File

@ -0,0 +1,55 @@
use crate::model::backup::backup_execution::*;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleState {
Active,
Paused,
Disabled,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleInterval {
Once,
Daily,
Weekly,
Monthly,
}
#[derive(Debug, Clone)]
pub struct BackupSchedule {
pub uuid: Uuid,
pub name: String,
pub state: ScheduleState,
pub source_path: PathBuf,
pub destination_path: PathBuf,
pub backup_type: BackupType,
pub comparison_mode: Option<ComparisonMode>,
pub options: BackupOptions,
pub interval: ScheduleInterval,
pub last_run_time: Option<NaiveDateTime>,
pub next_run_time: Option<NaiveDateTime>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
impl BackupSchedule {
pub fn to_execution(&self) -> BackupExecution {
BackupExecution {
uuid: self.uuid,
state: BackupState::Pending,
source_path: self.source_path.clone(),
destination_path: self.destination_path.clone(),
backup_type: if self.last_run_time.is_some() {
self.backup_type
} else {
BackupType::Full
},
comparison_mode: self.comparison_mode,
options: self.options,
}
}
}

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

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

View File

@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::model::error::Error;
#[derive(Serialize, Deserialize, Debug)]
pub struct ProgressData {
pub current_level: Vec<PathBuf>,
pub errors: Vec<Error>,
}
impl ProgressData {
pub fn new(current_level: Vec<PathBuf>, errors: Vec<Error>) -> ProgressData {
ProgressData {
current_level,
errors
}
}
}

View File

@ -8,6 +8,8 @@ pub struct ConfigTable {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Config {
pub internal_timestamp: i64, // mini second
pub default_wakeup_time: i64, // second
pub max_concurrency: u8, // number
pub max_file_operations: usize, // number
}

View File

@ -0,0 +1,24 @@
use macros::traceable;
traceable! {
DatabaseError {
#[error("Failed to create database")]
CreateDatabaseFailed => tracing::Level::ERROR,
#[error("Failed to connect to database")]
DatabaseConnectFailed => tracing::Level::ERROR,
#[error("Failed to lock database")]
LockDatabaseFailed => tracing::Level::ERROR,
#[error("Failed to unlock database")]
UnlockDatabaseFailed => tracing::Level::ERROR,
#[error("Failed to execute SQL statement")]
StatementExecutionFailed => tracing::Level::ERROR,
#[no_source]
#[error("Data is corrupted or in invalid format")]
DataCorrupted => tracing::Level::ERROR,
}
}

8
src/model/error/event.rs Normal file
View File

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

55
src/model/error/io.rs Normal file
View File

@ -0,0 +1,55 @@
use macros::traceable;
use std::path::PathBuf;
traceable! {
IOError {
#[error("Semaphore has been closed")]
SemaphoreClosed => tracing::Level::ERROR,
#[error("Failed to read symbol link: {path}")]
ReadSymbolLinkFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to create symbol link: From {src} To {dst}")]
CreateSymbolLinkFailed { src: PathBuf, dst: PathBuf } => tracing::Level::ERROR,
#[error("Failed to create directory: {path}")]
CreateDirectoryFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to read directory: {path}")]
ReadDirectoryFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to create file: {path}")]
CreateFileFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to read file: {path}")]
ReadFileFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to copy file: From {src} To {dst}")]
CopyFileFailed { src: PathBuf, dst: PathBuf } => tracing::Level::ERROR,
#[error("Failed to write file: {path}")]
WriteFileFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to delete directory: {path}")]
DeleteDirectoryFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to delete file: {path}")]
DeleteFileFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to get file metadata: {path}")]
GetMetadataFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to set file metadata: {path}")]
SetMetadataFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to lock file: {path}")]
LockFileFailed { path: PathBuf } => tracing::Level::ERROR,
#[error("Failed to unlock file: {path}")]
UnlockFileFailed { path: PathBuf } => tracing::Level::ERROR,
#[no_source]
#[error("File does not exist: {path}")]
FileDoesNotExist { path: PathBuf } => tracing::Level::ERROR,
}
}

17
src/model/error/misc.rs Normal file
View File

@ -0,0 +1,17 @@
use macros::traceable;
traceable! {
MiscError {
#[error("Failed to free object")]
ObjectFreeFailed => tracing::Level::ERROR,
#[error("Failed to serialize object")]
SerializeError => tracing::Level::ERROR,
#[error("Failed to deserialize object")]
DeserializeError => tracing::Level::ERROR,
#[error("Failed to initialize UI platform")]
UIPlatformError => tracing::Level::ERROR,
}
}

66
src/model/error/mod.rs Normal file
View File

@ -0,0 +1,66 @@
pub mod database;
pub mod event;
pub mod io;
pub mod misc;
pub mod system;
pub mod task;
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)]
pub enum Error {
#[error("{0}")]
Database(DatabaseError),
#[error("{0}")]
Event(EventError),
#[error("{0}")]
IO(IOError),
#[error("{0}")]
Misc(MiscError),
#[error("{0}")]
System(SystemError),
#[error("{0}")]
Task(TaskError),
}
impl From<DatabaseError> for Error {
fn from(error: DatabaseError) -> Self {
Self::Database(error)
}
}
impl From<EventError> for Error {
fn from(error: EventError) -> Self {
Self::Event(error)
}
}
impl From<IOError> for Error {
fn from(error: IOError) -> Self {
Self::IO(error)
}
}
impl From<MiscError> for Error {
fn from(error: MiscError) -> Self {
Self::Misc(error)
}
}
impl From<SystemError> for Error {
fn from(error: SystemError) -> Self {
Self::System(error)
}
}
impl From<TaskError> for Error {
fn from(error: TaskError) -> Self {
Self::Task(error)
}
}

35
src/model/error/system.rs Normal file
View File

@ -0,0 +1,35 @@
use macros::traceable;
traceable! {
SystemError {
#[no_source]
#[error("Unable to run as administrator")]
RunAsAdminFailed => tracing::Level::ERROR,
#[error("Failed to adjust token privileges")]
AdjustTokenPrivilegesFailed => tracing::Level::ERROR,
#[error("Invalid configuration")]
InvalidConfig => tracing::Level::ERROR,
#[error("Configuration not found")]
ConfigNotFound => tracing::Level::ERROR,
#[error("Failed to terminate instance")]
TerminateError => tracing::Level::ERROR,
#[no_source]
#[error("Failed to send shutdown signal")]
ShutdownSignalFailed => tracing::Level::ERROR,
#[error("Unexcepted thread panic")]
ThreadPanic => tracing::Level::ERROR,
#[error("Unexcepted error")]
UnexpectError => tracing::Level::ERROR,
#[no_source]
#[error("Unknown error")]
UnknownError => tracing::Level::ERROR,
}
}

16
src/model/error/task.rs Normal file
View File

@ -0,0 +1,16 @@
use macros::traceable;
traceable! {
TaskError {
#[no_source]
#[error("Illegal run state")]
IllegalRunState => tracing::Level::ERROR,
#[no_source]
#[error("Task not found")]
ExecutionNotFound => tracing::Level::ERROR,
#[error("Failed to stop task")]
StopExecutionFailed => tracing::Level::ERROR,
}
}

12
src/model/event/error.rs Normal file
View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,48 @@
use crate::interface::event::Event;
use crate::model::backup::backup_execution::{BackupExecution, BackupState};
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 ExecutionStateChanged {
pub execution_id: Uuid,
pub new_state: BackupState,
}
impl Event for ExecutionStateChanged {}
#[derive(Clone, Debug)]
pub struct ExecutionProgress {
pub task_id: Uuid,
pub processed_files: usize,
pub error_count: usize,
}
impl Event for ExecutionProgress {}

View File

@ -0,0 +1,10 @@
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,19 +0,0 @@
use crate::interface::event_system::event::Event;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone)]
pub struct GetAttributesEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for GetAttributesEvent {}
#[derive(Clone)]
pub struct SetAttributesEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for SetAttributesEvent {}

View File

@ -1,27 +0,0 @@
use crate::interface::event_system::event::Event;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone)]
pub struct ListDirectoryEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for ListDirectoryEvent {}
#[derive(Clone)]
pub struct CreateDirectoryEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for CreateDirectoryEvent {}
#[derive(Clone)]
pub struct DeleteDirectoryEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for DeleteDirectoryEvent {}

View File

@ -1,20 +0,0 @@
use crate::interface::event_system::event::Event;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone)]
pub struct CopyFileEvent {
pub task_id: Uuid,
pub source: PathBuf,
pub destination: PathBuf,
}
impl Event for CopyFileEvent {}
#[derive(Clone)]
pub struct DeleteFileEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for DeleteFileEvent {}

View File

@ -1,11 +0,0 @@
use crate::interface::event_system::event::Event;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone)]
pub struct CalculateHashEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for CalculateHashEvent {}

View File

@ -1,5 +0,0 @@
pub mod attributes;
pub mod directory;
pub mod file;
pub mod hash;
pub mod permission;

View File

@ -1,19 +0,0 @@
use crate::interface::event_system::event::Event;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Clone)]
pub struct GetPermissionEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for GetPermissionEvent {}
#[derive(Clone)]
pub struct SetPermissionEvent {
pub task_id: Uuid,
pub path: PathBuf,
}
impl Event for SetPermissionEvent {}

View File

@ -1,3 +1,4 @@
pub mod io;
pub mod progress;
pub mod task;
pub mod error;
pub mod execution;
pub mod filesystem;
pub mod schedule;

View File

@ -1,6 +0,0 @@
use crate::interface::event_system::event::Event;
#[derive(Clone)]
pub struct SaveCheckPointEvent {}
impl Event for SaveCheckPointEvent {}

View File

@ -1,2 +0,0 @@
pub mod checkpoint;
pub mod task;

View File

@ -1,6 +0,0 @@
use crate::interface::event_system::event::Event;
#[derive(Clone)]
pub struct UpdateProgressEvent {}
impl Event for UpdateProgressEvent {}

View File

@ -0,0 +1,39 @@
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,35 +0,0 @@
use crate::interface::event_system::event::Event;
#[derive(Clone)]
pub struct TaskCreateEvent {}
impl Event for TaskCreateEvent {}
#[derive(Clone)]
pub struct TaskStartEvent {}
impl Event for TaskStartEvent {}
#[derive(Clone)]
pub struct TaskPauseEvent {}
impl Event for TaskPauseEvent {}
#[derive(Clone)]
pub struct TaskResumeEvent {}
impl Event for TaskResumeEvent {}
#[derive(Clone)]
pub struct TaskCompleteEvent {}
impl Event for TaskCompleteEvent {}
#[derive(Clone)]
pub struct TaskCancelEvent {}
impl Event for TaskCancelEvent {}
#[derive(Clone)]
pub struct TaskFailEvent {}
impl Event for TaskFailEvent {}

View File

@ -0,0 +1,8 @@
use macros::loggable;
loggable! {
DatabaseLog {
#[error("Connected to database successfully")]
DatabaseConnectSuccess => tracing::Level::INFO,
}
}

8
src/model/log/event.rs Normal file
View File

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

8
src/model/log/io.rs Normal file
View File

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

8
src/model/log/misc.rs Normal file
View File

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

6
src/model/log/mod.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod database;
pub mod event;
pub mod io;
pub mod misc;
pub mod system;
pub mod task;

23
src/model/log/system.rs Normal file
View File

@ -0,0 +1,23 @@
use macros::loggable;
loggable! {
SystemLog {
#[error("Rerun the program as administrator")]
ReRunAsAdmin => tracing::Level::WARN,
#[error("Initializing")]
Initializing => tracing::Level::INFO,
#[error("Initialization completed")]
InitializeComplete => tracing::Level::INFO,
#[error("Termination in process")]
Terminating => tracing::Level::INFO,
#[error("Termination completed")]
TerminateComplete => tracing::Level::INFO,
#[error("Gui Exited")]
GuiExited => tracing::Level::INFO,
}
}

8
src/model/log/task.rs Normal file
View File

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

View File

@ -1,3 +1,5 @@
pub mod task;
pub mod backup;
pub mod config;
pub mod error;
pub mod event;
pub mod log;

View File

@ -1,14 +1,45 @@
#[derive(Clone, PartialEq, Eq)]
pub struct Attributes {
use libc::{gid_t, mode_t, uid_t};
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq)]
pub struct Attributes {
pub attributes: u32,
pub creation_time: SystemTime,
pub last_access_time: SystemTime,
pub change_time: SystemTime,
}
impl PartialEq for Attributes {
fn eq(&self, _: &Attributes) -> bool {
true
#[derive(Debug, Clone, PartialEq)]
pub struct Permissions {
pub uid: uid_t,
pub gid: gid_t,
pub mode: u32,
pub is_sticky: bool,
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,
}
}
}
pub struct Permissions {
#[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,3 +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";

View File

@ -1,19 +0,0 @@
use crate::interface::database_ops::DatabaseOpsTrait;
use async_trait::async_trait;
use sqlx::SqlitePool;
#[derive(Clone, Debug)]
pub struct DatabaseOps {
pool: SqlitePool,
}
#[async_trait]
impl DatabaseOpsTrait for DatabaseOps {
fn new(pool: SqlitePool) -> Self {
DatabaseOps { pool }
}
fn get_pool(&self) -> SqlitePool {
self.pool.clone()
}
}

View File

@ -1,9 +1,17 @@
use crate::core::event_bus::event_bus::EventBus;
use crate::interface::file_system::FileSystemTrait;
use crate::model::error::io::IOError;
use crate::model::error::system::SystemError;
use crate::platform::attributes::{Attributes, Permissions};
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::sync::Arc;
use std::time::SystemTime;
use tokio::sync::Semaphore;
use tokio::task::spawn_blocking;
use uuid::Uuid;
pub struct FileSystem {
@ -11,48 +19,231 @@ pub struct FileSystem {
}
#[async_trait]
#[cfg(target_os = "linux")]
impl FileSystemTrait for FileSystem {
fn new(semaphore: Arc<Semaphore>) -> Self {
FileSystem { semaphore }
Self { semaphore }
}
fn semaphore(&self) -> Arc<Semaphore> {
self.semaphore.clone()
}
async fn get_attributes(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<Attributes> {
todo!()
async fn create_symlink(&self, target: &PathBuf, link_path: &PathBuf) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOError::SemaphoreClosed)?;
tokio::fs::symlink(target, link_path).await.map_err(|_| {
IOError::CreateSymbolLinkFailed {
src: target.clone(),
dst: link_path.clone(),
}
})?;
Ok(())
}
async fn set_attributes(
async fn copy_symlink(
&self,
task_id: Uuid,
path: PathBuf,
attributes: Attributes,
) -> anyhow::Result<()> {
todo!()
source_link: &PathBuf,
destination_link: &PathBuf,
) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOError::SemaphoreClosed)?;
tokio::fs::symlink(&link_target, destination_link)
.await
.map_err(|_| IOError::CreateSymbolLinkFailed {
src: source_link.clone(),
dst: destination_link.clone(),
})?;
}
async fn compare_attributes(
&self,
task_id: Uuid,
source: PathBuf,
destination: PathBuf,
) -> anyhow::Result<bool> {
todo!()
async fn get_attributes(&self, path: &PathBuf) -> Result<Attributes, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOError::SemaphoreClosed)?;
let metadata = tokio::fs::metadata(path)
.await
.map_err(|_| IOError::GetMetadataFailed)?;
let mode = metadata.mode();
let file_type = metadata.file_type();
let mut attributes = 0_u32;
if file_type.is_dir() {
attributes |= libc::S_IFDIR;
} else if file_type.is_file() {
attributes |= libc::S_IFREG;
} else if file_type.is_symlink() {
attributes |= libc::S_IFLNK;
}
attributes |= mode & 0o777;
let creation_time = metadata
.created()
.unwrap_or_else(|_| metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH));
let last_access_time = metadata
.accessed()
.map_err(|_| IOError::GetMetadataFailed)?;
let change_time = metadata
.modified()
.map_err(|_| IOError::GetMetadataFailed)?;
let attributes = Attributes {
attributes,
creation_time,
last_access_time,
change_time,
};
Ok(attributes)
}
async fn get_permission(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<Permissions> {
todo!()
async fn set_attributes(&self, path: &PathBuf, attributes: Attributes) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOError::SemaphoreClosed)?;
let path_clone = path.clone();
let mode = attributes.attributes & 0o7777;
spawn_blocking(move || {
let path = path_clone;
let c_path = CString::new(path.to_string_lossy().as_bytes())
.map_err(|_| IOError::SetMetadataFailed)?;
unsafe {
if libc::chmod(c_path.as_ptr(), mode as mode_t) != 0 {
Err(IOError::SetMetadataFailed)?;
}
}
Self::set_file_times(&path, &attributes)?;
Ok::<(), Error>(())
})
.await
.map_err(|_| SystemError::ThreadPanic)??;
Ok(())
}
async fn set_permission(
&self,
task_id: Uuid,
path: PathBuf,
permission: Permissions,
) -> anyhow::Result<()> {
todo!()
async fn get_permission(&self, path: &PathBuf) -> Result<Permissions, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOError::SemaphoreClosed)?;
let path_clone = path.clone();
let permission = spawn_blocking(move || {
let path = path_clone;
let metadata = std::fs::metadata(&path).map_err(|_| IOError::GetMetadataFailed)?;
let uid = metadata.uid();
let gid = metadata.gid();
let mode = metadata.mode();
Ok::<Permissions, Error>(Permissions {
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,
})
})
.await
.map_err(|_| SystemError::ThreadPanic)??;
Ok(permission)
}
async fn set_permission(&self, path: &PathBuf, permissions: Permissions) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOError::SemaphoreClosed)?;
let path_clone = path.clone();
spawn_blocking(move || {
let path = path_clone;
let c_path = CString::new(path.to_string_lossy().as_bytes())
.map_err(|_| IOError::SetMetadataFailed)?;
unsafe {
if libc::chown(c_path.as_ptr(), permissions.uid, permissions.gid) != 0 {
return Err(IOError::SetMetadataFailed.into());
}
let mut mode = permissions.mode & 0o7777;
if permissions.is_sticky {
mode |= libc::S_ISVTX as u32;
}
if permissions.is_setuid {
mode |= libc::S_ISUID as u32;
}
if permissions.is_setgid {
mode |= libc::S_ISGID as u32;
}
if libc::chmod(c_path.as_ptr(), mode as mode_t) != 0 {
return Err(IOError::SetMetadataFailed.into());
}
}
Ok::<(), Error>(())
})
.await
.map_err(|_| SystemError::ThreadPanic)??;
Ok(())
}
}
impl FileSystem {
fn set_file_times(path: &PathBuf, attributes: &Attributes) -> Result<(), Error> {
let c_path = CString::new(path.to_string_lossy().as_bytes())
.map_err(|_| IOError::SetMetadataFailed)?;
let access_time = Self::system_time_to_timespec(attributes.last_access_time)?;
let modify_time = Self::system_time_to_timespec(attributes.change_time)?;
let times = [access_time, modify_time];
unsafe {
if libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), 0) != 0 {
return Err(IOError::SetMetadataFailed.into());
}
}
Ok(())
}
fn system_time_to_timespec(system_time: SystemTime) -> Result<libc::timespec, Error> {
let duration = system_time
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| SystemError::UnexpectError)?;
Ok(libc::timespec {
tv_sec: duration.as_secs() as libc::time_t,
tv_nsec: duration.subsec_nanos() as libc::c_long,
})
}
}

View File

@ -1,5 +1,4 @@
pub mod attributes;
pub mod constants;
pub mod database_ops;
pub mod elevate;
pub mod file_system;

View File

@ -1,14 +1,10 @@
use crate::platform::raii_guard::SecurityDescriptorGuard;
use std::time::SystemTime;
use windows::Win32::Security::{ACL, PSID};
use crate::platform::raii_guard::SecurityDescriptorGuard;
#[derive(Clone, Eq)]
pub struct Attributes {
pub read_only: bool,
pub hidden: bool,
pub archive: bool,
pub normal: bool,
pub index: bool,
pub attributes: u32,
pub creation_time: SystemTime,
pub last_access_time: SystemTime,
pub change_time: SystemTime,
@ -16,13 +12,7 @@ pub struct Attributes {
impl PartialEq for Attributes {
fn eq(&self, other: &Self) -> bool {
self.read_only == other.read_only
&& self.hidden == other.hidden
&& self.archive == other.archive
&& self.normal == other.normal
&& self.index == other.index
&& self.creation_time == other.creation_time
&& self.last_access_time == other.last_access_time
self.attributes == other.attributes
}
}

View File

@ -1,3 +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";

View File

@ -1,17 +0,0 @@
use crate::interface::database_ops::DatabaseOpsTrait;
use sqlx::SqlitePool;
#[derive(Clone, Debug)]
pub struct DatabaseOps {
pool: SqlitePool,
}
impl DatabaseOpsTrait for DatabaseOps {
fn new(pool: SqlitePool) -> Self {
DatabaseOps { pool }
}
fn get_pool(&self) -> SqlitePool {
self.pool.clone()
}
}

View File

@ -1,9 +1,11 @@
use crate::utils::log_entry::system::SystemEntry;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError;
use crate::model::error::Error;
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::{env, mem};
use windows::core::PCWSTR;
use windows::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, LUID};
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Security::{
AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES, SE_PRIVILEGE_ENABLED,
SE_SECURITY_NAME, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES, TOKEN_QUERY,
@ -15,8 +17,9 @@ use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use windows::Win32::UI::Shell::{ShellExecuteExW, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW};
use windows::Win32::UI::WindowsAndMessaging::SW_NORMAL;
pub fn elevate() -> anyhow::Result<()> {
let exe = env::current_exe()?;
pub fn elevate() -> Result<(), Error> {
let exe = env::current_exe()
.map_err(|err| SystemError::UnexpectError(err))?;
let args: Vec<String> = env::args().skip(1).collect();
let params = args.join(" ");
@ -36,69 +39,73 @@ pub fn elevate() -> anyhow::Result<()> {
}
}
unsafe fn win_runas(cmd: Vec<u16>, args: Vec<u16>) -> anyhow::Result<()> {
let mut sei: SHELLEXECUTEINFOW = mem::zeroed();
let verb = "runas\0".encode_utf16().collect::<Vec<u16>>();
unsafe fn win_runas(cmd: Vec<u16>, args: Vec<u16>) -> Result<(), Error> {
unsafe {
let mut sei: SHELLEXECUTEINFOW = mem::zeroed();
let verb = "runas\0".encode_utf16().collect::<Vec<u16>>();
if CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE).is_err() {
Err(SystemEntry::RunAsAdminFailed)?
if CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE).is_err() {
Err(SystemError::RunAsAdminFailed)?
}
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.cbSize = size_of::<SHELLEXECUTEINFOW>() as u32;
sei.lpVerb = PCWSTR(verb.as_ptr());
sei.lpFile = PCWSTR(cmd.as_ptr());
sei.lpParameters = PCWSTR(args.as_ptr());
sei.nShow = SW_NORMAL.0;
if ShellExecuteExW(&mut sei).is_err() || sei.hProcess.is_invalid() {
Err(SystemError::RunAsAdminFailed)?
}
Ok(())
}
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.cbSize = size_of::<SHELLEXECUTEINFOW>() as u32;
sei.lpVerb = PCWSTR(verb.as_ptr());
sei.lpFile = PCWSTR(cmd.as_ptr());
sei.lpParameters = PCWSTR(args.as_ptr());
sei.nShow = SW_NORMAL.0;
if ShellExecuteExW(&mut sei).is_err() || sei.hProcess.is_invalid() {
Err(SystemEntry::RunAsAdminFailed)?
}
Ok(())
}
unsafe fn adjust_token_privileges() -> anyhow::Result<()> {
let mut token_handle: HANDLE = HANDLE::default();
unsafe fn adjust_token_privileges() -> Result<(), Error> {
unsafe {
let mut token_handle: HANDLE = HANDLE::default();
OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&mut token_handle,
)
.map_err(|_| SystemEntry::AdjustTokenPrivilegesFailed)?;
OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&mut token_handle,
)
.map_err(|err| SystemError::AdjustTokenPrivilegesFailed(err))?;
let mut luid = LUID {
LowPart: 0,
HighPart: 0,
};
let mut luid = LUID {
LowPart: 0,
HighPart: 0,
};
LookupPrivilegeValueW(PCWSTR::null(), SE_SECURITY_NAME, &mut luid)
.map_err(|_| SystemEntry::AdjustTokenPrivilegesFailed)?;
LookupPrivilegeValueW(PCWSTR::null(), SE_SECURITY_NAME, &mut luid)
.map_err(|err| SystemError::AdjustTokenPrivilegesFailed(err))?;
let mut token_privilege = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
let mut token_privilege = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
AdjustTokenPrivileges(
token_handle,
false,
Some(&mut token_privilege),
0,
None,
None,
)
.map_err(|_| SystemEntry::AdjustTokenPrivilegesFailed)?;
AdjustTokenPrivileges(
token_handle,
false,
Some(&mut token_privilege),
0,
None,
None,
)
.map_err(|err| SystemError::AdjustTokenPrivilegesFailed(err))?;
CloseHandle(token_handle).map_err(|_| SystemEntry::AdjustTokenPrivilegesFailed)?;
CloseHandle(token_handle).map_err(|err| MiscError::ObjectFreeFailed(err))?;
if GetLastError().is_err() {
Err(SystemEntry::AdjustTokenPrivilegesFailed)?
// if GetLastError().is_err() {
// Err(SystemError::AdjustTokenPrivilegesFailed)?
// }
Ok(())
}
Ok(())
}

View File

@ -1,11 +1,10 @@
use crate::core::event_system::event_bus::EventBus;
use crate::interface::file_system::FileSystemTrait;
use crate::model::event::io::attributes::{GetAttributesEvent, SetAttributesEvent};
use crate::model::event::io::permission::GetPermissionEvent;
use crate::model::error::io::IOError;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError;
use crate::model::error::Error;
use crate::platform::attributes::{Attributes, Permissions};
use crate::platform::raii_guard::SecurityDescriptorGuard;
use crate::utils::log_entry::io::IOEntry;
use crate::utils::log_entry::system::SystemEntry;
use async_trait::async_trait;
use chrono::{DateTime, Datelike, Timelike};
use std::os::windows::ffi::OsStrExt;
@ -16,7 +15,6 @@ use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::Semaphore;
use tokio::task::spawn_blocking;
use uuid::Uuid;
use windows::core::PCWSTR;
use windows::Win32::Foundation::{CloseHandle, FILETIME, GENERIC_ALL, SYSTEMTIME};
use windows::Win32::Security::Authorization::{
@ -24,104 +22,119 @@ use windows::Win32::Security::Authorization::{
};
use windows::Win32::Security::{ACL, BACKUP_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID};
use windows::Win32::Storage::FileSystem::{
CreateFileW, SetFileAttributesW, SetFileTime, FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_HIDDEN,
FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, FILE_ATTRIBUTE_READONLY,
FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
CreateFileW, SetFileAttributesW, SetFileTime, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_DELETE,
FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows::Win32::System::Time::SystemTimeToFileTime;
#[cfg(target_os = "windows")]
pub struct FileSystem {
semaphore: Arc<Semaphore>,
}
#[async_trait]
#[cfg(target_os = "windows")]
impl FileSystemTrait for FileSystem {
fn new(semaphore: Arc<Semaphore>) -> Self {
FileSystem { semaphore }
Self { semaphore }
}
fn semaphore(&self) -> Arc<Semaphore> {
self.semaphore.clone()
}
async fn get_attributes(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<Attributes> {
async fn create_symlink(&self, target: &PathBuf, link_path: &PathBuf) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
.map_err(|err| IOError::SemaphoreClosed(err))?;
let metadata = tokio::fs::metadata(&path)
if link_path.is_dir() {
tokio::fs::symlink_dir(target, link_path).await
} else {
tokio::fs::symlink_file(target, link_path).await
}
.map_err(|err| IOError::CreateSymbolLinkFailed(target.clone(), link_path.clone(), err))?;
Ok(())
}
async fn copy_symlink(
&self,
source_link: &PathBuf,
destination_link: &PathBuf,
) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::GetMetadataFailed)?;
.map_err(|err| IOError::SemaphoreClosed(err))?;
let (read_only, hidden, archive, normal, index) = {
let attributes = metadata.file_attributes();
(
(attributes & FILE_ATTRIBUTE_READONLY.0) != 0,
(attributes & FILE_ATTRIBUTE_HIDDEN.0) != 0,
(attributes & FILE_ATTRIBUTE_ARCHIVE.0) != 0,
(attributes & FILE_ATTRIBUTE_NORMAL.0) != 0,
(attributes & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED.0) != 0,
)
};
let link_target =
tokio::fs::read_link(source_link)
.await
.map_err(|err| IOError::ReadSymbolLinkFailed(source_link.clone(), err))?;
if link_target.is_dir() {
tokio::fs::symlink_dir(&link_target, destination_link).await
} else {
tokio::fs::symlink_file(&link_target, destination_link).await
}
.map_err(|err| IOError::CreateSymbolLinkFailed(link_target, destination_link, err))?;
Ok(())
}
async fn get_attributes(&self, path: &PathBuf) -> Result<Attributes, Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|err| IOError::SemaphoreClosed(err))?;
let metadata = tokio::fs::metadata(path)
.await
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
let attributes = metadata.file_attributes();
let creation_time = metadata
.created()
.map_err(|_| IOEntry::GetMetadataFailed)?;
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
let last_access_time = metadata
.accessed()
.map_err(|_| IOEntry::GetMetadataFailed)?;
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
let change_time = metadata
.modified()
.map_err(|_| IOEntry::GetMetadataFailed)?;
.map_err(|err| IOError::GetMetadataFailed(path.clone(), err))?;
let attributes = Attributes {
read_only,
hidden,
archive,
normal,
index,
attributes,
creation_time,
last_access_time,
change_time,
};
let event = GetAttributesEvent { task_id, path };
EventBus::publish(event).await?;
Ok(attributes)
}
async fn set_attributes(
&self,
task_id: Uuid,
path: PathBuf,
attributes: Attributes,
) -> anyhow::Result<()> {
async fn set_attributes(&self, path: &PathBuf, attributes: Attributes) -> Result<(), Error> {
let semaphore = self.semaphore();
let _permit = semaphore
.acquire_owned()
.await
.map_err(|_| IOEntry::SemaphoreClosed)?;
.map_err(|err| IOError::SemaphoreClosed(err))?;
let file_path_wild: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let mut file_attributes: u32 = 0;
file_attributes |= FILE_ATTRIBUTE_READONLY.0;
file_attributes |= FILE_ATTRIBUTE_HIDDEN.0;
file_attributes |= FILE_ATTRIBUTE_ARCHIVE.0;
file_attributes |= FILE_ATTRIBUTE_NORMAL.0;
file_attributes |= FILE_ATTRIBUTE_NOT_CONTENT_INDEXED.0;
let file_attributes = attributes.attributes;
let path = path.clone();
spawn_blocking(move || unsafe {
SetFileAttributesW(
PCWSTR(file_path_wild.as_ptr()),
FILE_FLAGS_AND_ATTRIBUTES(file_attributes),
)
.map_err(|_| IOEntry::SetMetadataFailed)?;
.map_err(|err| IOError::SetMetadataFailed(path.clone(), err))?;
let handle = CreateFileW(
PCWSTR(file_path_wild.as_ptr()),
@ -132,7 +145,7 @@ impl FileSystemTrait for FileSystem {
FILE_FLAGS_AND_ATTRIBUTES(file_attributes),
None,
)
.map_err(|_| IOEntry::SetMetadataFailed)?;
.map_err(|err| IOError::SetMetadataFailed(path.clone(), err))?;
let creation_filetime = Self::system_time_to_file_time(attributes.creation_time)?;
let last_access_filetime = Self::system_time_to_file_time(attributes.last_access_time)?;
@ -145,36 +158,22 @@ impl FileSystemTrait for FileSystem {
Some(&change_filetime),
);
CloseHandle(handle)
.map_err(|_| SystemEntry::ObjectFreeFailed)?;
CloseHandle(handle).map_err(|err| MiscError::ObjectFreeFailed(err))?;
result
.map_err(|_| IOEntry::SetMetadataFailed)?;
Ok::<(), anyhow::Error>(())
result.map_err(|err| IOError::SetMetadataFailed(path.clone(), err))?;
Ok::<(), Error>(())
})
.await
.map_err(|_| SystemEntry::ThreadPanic)??;
.map_err(|err| SystemError::ThreadPanic(err))??;
let event = SetAttributesEvent { task_id, path };
EventBus::publish(event).await?;
Ok(())
}
async fn compare_attributes(
&self,
task_id: Uuid,
source: PathBuf,
destination: PathBuf,
) -> anyhow::Result<bool> {
let source_attributes = self.get_attributes(task_id, source).await?;
let destination_attributes = self.get_attributes(task_id, destination).await?;
Ok(source_attributes == destination_attributes)
}
async fn get_permission(&self, task_id: Uuid, path: PathBuf) -> anyhow::Result<Permissions> {
async fn get_permission(&self, path: &PathBuf) -> Result<Permissions, Error> {
let file_path_wild: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let path = path.clone();
let permission = spawn_blocking(move || unsafe {
let security_info = BACKUP_SECURITY_INFORMATION;
let mut owner = PSID::default();
@ -183,7 +182,7 @@ impl FileSystemTrait for FileSystem {
let mut sacl: *mut ACL = ptr::null_mut();
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
if GetNamedSecurityInfoW(
let result = GetNamedSecurityInfoW(
PCWSTR(file_path_wild.as_ptr()),
SE_FILE_OBJECT,
security_info,
@ -192,13 +191,13 @@ impl FileSystemTrait for FileSystem {
Some(&mut dacl),
Some(&mut sacl),
&mut security_descriptor,
)
.is_err()
{
Err(IOEntry::GetMetadataFailed)?;
);
if result.is_err() {
Err(IOError::GetMetadataFailed(path.clone(), format!("{:?}", result)))?;
}
Ok::<Permissions, anyhow::Error>(Permissions {
Ok::<Permissions, Error>(Permissions {
owner,
primary_group,
dacl,
@ -207,19 +206,12 @@ impl FileSystemTrait for FileSystem {
})
})
.await
.map_err(|_| SystemEntry::ThreadPanic)??;
.map_err(|err| SystemError::ThreadPanic(err))??;
let event = GetPermissionEvent { task_id, path };
EventBus::publish(event).await?;
Ok(permission)
}
async fn set_permission(
&self,
task_id: Uuid,
path: PathBuf,
permissions: Permissions,
) -> anyhow::Result<()> {
async fn set_permission(&self, path: &PathBuf, permissions: Permissions) -> Result<(), Error> {
let file_path_wild: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let security_info = BACKUP_SECURITY_INFORMATION;
@ -230,7 +222,7 @@ impl FileSystemTrait for FileSystem {
let security_descriptor = permissions.security_descriptor;
unsafe {
if SetNamedSecurityInfoW(
let result = SetNamedSecurityInfoW(
PCWSTR(file_path_wild.as_ptr()),
SE_FILE_OBJECT,
security_info,
@ -238,29 +230,27 @@ impl FileSystemTrait for FileSystem {
Some(primary_group),
Some(dacl),
Some(sacl),
)
.is_err()
{
Err(IOEntry::SetMetadataFailed)?;
);
if result.is_err() {
Err(IOError::SetMetadataFailed(path.clone(), format!("{:?}", result)))?;
}
}
drop(security_descriptor);
let event = SetAttributesEvent { task_id, path };
EventBus::publish(event).await?;
Ok(())
}
}
impl FileSystem {
fn system_time_to_file_time(system_time: SystemTime) -> anyhow::Result<FILETIME> {
fn system_time_to_file_time(system_time: SystemTime) -> Result<FILETIME, Error> {
let duration = system_time
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| SystemEntry::InternalError)?;
.map_err(|err| SystemError::UnexpectError(err))?;
let epoch = DateTime::from_timestamp(duration.as_secs() as i64, duration.subsec_nanos())
.ok_or_else(|| SystemEntry::InternalError)?;
.ok_or(SystemError::UnknownError)?;
let sys_time = SYSTEMTIME {
wYear: epoch.year() as u16,
@ -277,7 +267,7 @@ impl FileSystem {
unsafe {
SystemTimeToFileTime(&sys_time, &mut file_time)
.map_err(|_| SystemEntry::InternalError)?;
.map_err(|err| SystemError::UnexpectError(err))?;
Ok(file_time)
}
}

View File

@ -1,6 +1,5 @@
pub mod attributes;
pub mod constants;
pub mod database_ops;
pub mod elevate;
pub mod file_system;
pub mod raii_guard;

454
src/ui/execution_page.rs Normal file
View File

@ -0,0 +1,454 @@
use crate::core::backup_engine::BackupEngine;
use crate::core::event_bus::EventBus;
use crate::model::backup::backup_execution::*;
use crate::model::error::Error;
use crate::model::event::error::BackupError;
use crate::model::event::execution::*;
use crate::model::event::filesystem::FolderProcessing;
use dashmap::DashMap;
use eframe::egui;
use egui_file_dialog::FileDialog;
use futures::executor::block_on;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use uuid::Uuid;
use crate::core::app_config::AppConfig;
#[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 {
config: Arc<AppConfig>,
backup_engine: Arc<BackupEngine>,
folder_processing_events: Receiver<FolderProcessing>,
progress_events: Receiver<ExecutionProgress>,
backup_error_events: Receiver<BackupError>,
executions: DashMap<Uuid, ExecutionDisplay>,
error_messages: DashMap<Uuid, Vec<Error>>,
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,
show_add_task_dialog: bool,
file_dialog: FileDialog,
folder_selection_mode: Option<FolderSelectionMode>,
pub auto_scroll_errors: bool,
pub show_completed_tasks: bool,
viewing_errors_for_task: Option<Uuid>,
}
impl ExecutionPage {
pub fn new(config: Arc<AppConfig>, event_bus: Arc<EventBus>, backup_engine: Arc<BackupEngine>) -> 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 {
config,
backup_engine,
folder_processing_events,
progress_events,
backup_error_events,
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,
show_add_task_dialog: false,
file_dialog: FileDialog::new(),
folder_selection_mode: None,
auto_scroll_errors: true,
show_completed_tasks: true,
viewing_errors_for_task: None,
}
}
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]);
}
}
}
}
pub fn update(&mut self, ctx: &egui::Context) {
self.process_events();
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Backup Executions");
ui.horizontal(|ui| {
if ui.button(" Add Execution").clicked() {
self.show_add_task_dialog = true;
}
ui.separator();
let running_count = self
.executions
.iter()
.filter(|entry| entry.value().execution.state == BackupState::Running)
.count();
ui.label(format!("Running: {}", running_count));
let completed_count = self
.executions
.iter()
.filter(|entry| entry.value().execution.state == BackupState::Completed)
.count();
ui.label(format!("Completed: {}", completed_count));
let error_count: usize = self
.error_messages
.iter()
.map(|entry| entry.value().len())
.sum();
if error_count > 0 {
ui.separator();
ui.colored_label(egui::Color32::RED, format!("Total Errors: {}", error_count));
}
});
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
let tasks_to_show: Vec<(Uuid, ExecutionDisplay)> = self
.executions
.iter()
.filter_map(|entry| {
let (task_id, task_display) = (entry.key(), entry.value());
if !self.show_completed_tasks
&& task_display.execution.state == BackupState::Completed
{
return None;
}
Some((*task_id, task_display.clone()))
})
.collect();
for (task_id, task_display) in tasks_to_show {
self.draw_execution_item(ui, task_id, &task_display);
ui.separator();
}
if self.executions.is_empty() {
ui.vertical_centered(|ui| {
ui.label("🚀 No backup executions");
ui.label("Click the button above to add an execution");
});
}
});
});
self.draw_add_execution_dialog(ctx);
self.draw_execution_errors_window(ctx);
}
fn draw_execution_item(&mut self, ui: &mut egui::Ui, task_id: Uuid, task_display: &ExecutionDisplay) {
egui::Frame::new()
.fill(ui.visuals().faint_bg_color)
.inner_margin(8.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(format!("🗂️ {}", task_display.execution.source_path.display()));
ui.label(format!("📁 {}", task_display.execution.destination_path.display()));
ui.horizontal(|ui| {
let (color, symbol) = match task_display.execution.state {
BackupState::Running => (egui::Color32::GREEN, "▶️"),
BackupState::Suspended => (egui::Color32::YELLOW, "⏸️"),
BackupState::Completed => (egui::Color32::BLUE, ""),
BackupState::Failed => (egui::Color32::RED, ""),
BackupState::Canceled => (egui::Color32::GRAY, "⏹️"),
BackupState::Pending => (egui::Color32::GRAY, "⏸️"),
};
ui.colored_label(color, format!("{} {:?}", symbol, task_display.execution.state));
if !task_display.current_folder.is_empty() {
ui.separator();
ui.label(format!("📄 {}",
task_display.current_folder.chars().take(50).collect::<String>()));
}
});
ui.horizontal(|ui| {
if task_display.processed_files > 0 || task_display.error_count > 0 {
ui.label(format!("📊 Processed: {} | Errors: {}",
task_display.processed_files, task_display.error_count));
}
});
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if let Some(errors) = self.error_messages.get(&task_id) {
if !errors.is_empty() {
if ui.small_button("👁 View Errors").clicked() {
self.viewing_errors_for_task = Some(task_id);
}
ui.separator();
}
}
match task_display.execution.state {
BackupState::Pending | BackupState::Suspended => {
if ui.button("▶️ Start").clicked() {
if let Err(e) = block_on(self.backup_engine.start_execution(task_id)) {
eprintln!("Failed to start execution: {:?}", e);
}
}
}
BackupState::Running => {
if ui.button("⏸️ Pause").clicked() {
if let Err(e) = block_on(self.backup_engine.suspend_execution(task_id)) {
eprintln!("Failed to suspend execution: {:?}", e);
}
}
}
_ => {}
}
if task_display.execution.state == BackupState::Suspended {
if ui.button("▶️ Resume").clicked() {
let rt = tokio::runtime::Handle::current();
if let Err(e) = rt.block_on(self.backup_engine.resume_execution(task_id)) {
eprintln!("Failed to resume execution: {:?}", e);
}
}
}
if ui.button("🗑️").clicked() {
let rt = tokio::runtime::Handle::current();
rt.block_on(self.backup_engine.remove_execution(&task_id));
self.executions.remove(&task_id);
self.error_messages.remove(&task_id);
if self.viewing_errors_for_task == Some(task_id) {
self.viewing_errors_for_task = None;
}
}
});
});
});
}
fn draw_add_execution_dialog(&mut self, ctx: &egui::Context) {
if self.show_add_task_dialog {
egui::Window::new("Add Backup Execution")
.collapsible(false)
.resizable(false)
.show(ctx, |ui| {
egui::Grid::new("add_execution_grid")
.num_columns(3)
.spacing([10.0, 4.0])
.show(ui, |ui| {
ui.label("Source Path:");
ui.text_edit_singleline(&mut self.new_task_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_task_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.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");
ui.separator();
ui.horizontal(|ui| {
if ui.button("Create Execution").clicked() {
if !self.new_task_source.is_empty() && !self.new_task_destination.is_empty() {
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,
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,
},
};
let execution_display = ExecutionDisplay::from(execution.clone());
self.executions.insert(execution.uuid, execution_display);
let rt = tokio::runtime::Handle::current();
rt.block_on(self.backup_engine.add_execution(execution));
self.reset_form();
}
}
if ui.button("Cancel").clicked() {
self.show_add_task_dialog = false;
}
});
});
}
self.file_dialog.update(ctx);
if let Some(path) = self.file_dialog.take_picked() {
if let Some(mode) = &self.folder_selection_mode {
match mode {
FolderSelectionMode::Source => {
self.new_task_source = path.to_string_lossy().to_string();
}
FolderSelectionMode::Destination => {
self.new_task_destination = path.to_string_lossy().to_string();
}
}
}
self.folder_selection_mode = None;
}
}
fn draw_execution_errors_window(&mut self, ctx: &egui::Context) {
if let Some(task_id) = self.viewing_errors_for_task {
let mut show_window = true;
let window_title = if let Some(task) = self.executions.get(&task_id) {
format!("Execution Errors - {}",
task.execution.source_path.file_name().unwrap_or_default().to_string_lossy())
} else {
"Execution Errors".to_string()
};
egui::Window::new(window_title)
.open(&mut show_window)
.resizable(true)
.default_width(600.0)
.default_height(400.0)
.show(ctx, |ui| {
if let Some(errors) = self.error_messages.get(&task_id) {
ui.horizontal(|ui| {
ui.heading(format!("Error List ({} items)", errors.len()));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("🗑️ Clear All Errors").clicked() {
self.error_messages.remove(&task_id);
self.viewing_errors_for_task = None;
}
});
});
ui.separator();
egui::ScrollArea::vertical()
.stick_to_bottom(self.auto_scroll_errors)
.show(ui, |ui| {
for (i, error) in errors.iter().enumerate() {
egui::Frame::new()
.fill(if i % 2 == 0 {
ui.visuals().faint_bg_color
} else {
egui::Color32::TRANSPARENT
})
.inner_margin(4.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(format!("{}.", i + 1));
ui.colored_label(egui::Color32::LIGHT_RED, format!("{}", error));
});
});
}
if errors.is_empty() {
ui.vertical_centered(|ui| {
ui.label("✅ No errors for this execution");
});
}
});
} else {
ui.vertical_centered(|ui| {
ui.label("⚠️ Cannot find error information for this execution");
});
}
});
if !show_window {
self.viewing_errors_for_task = None;
}
}
}
fn reset_form(&mut self) {
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.show_add_task_dialog = false;
}
}

89
src/ui/main_page.rs Normal file
View File

@ -0,0 +1,89 @@
use crate::core::backup_engine::BackupEngine;
use crate::core::event_bus::EventBus;
use crate::core::schedule_manager::ScheduleManager;
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,
execution_page: ExecutionPage,
schedule_page: SchedulePage,
}
impl MainPage {
pub fn new(
config: Arc<AppConfig>,
event_bus: Arc<EventBus>,
backup_engine: Arc<BackupEngine>,
schedule_manager: Arc<ScheduleManager>,
) -> Self {
Self {
current_page: PageType::Executions,
execution_page: ExecutionPage::new(config.clone(), event_bus, backup_engine),
schedule_page: SchedulePage::new(config, schedule_manager),
}
}
fn draw_top_panel(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(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");
}
}
});
});
});
}
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::Schedules, "⏰ Schedules");
});
});
}
}
impl App for MainPage {
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
self.draw_top_panel(ctx);
self.draw_tabs(ctx);
match self.current_page {
PageType::Executions => self.execution_page.update(ctx),
PageType::Schedules => self.schedule_page.update(ctx),
}
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
log!(SystemLog::GuiExited)
}
}

View File

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

449
src/ui/schedule_page.rs Normal file
View File

@ -0,0 +1,449 @@
use crate::core::schedule_manager::ScheduleManager;
use crate::model::backup::backup_execution::*;
use crate::model::backup::backup_schedule::*;
use eframe::egui;
use egui_file_dialog::FileDialog;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use futures::executor::block_on;
use uuid::Uuid;
use crate::core::app_config::AppConfig;
#[derive(Debug, Clone, PartialEq)]
enum FolderSelectionMode {
Source,
Destination,
}
pub struct SchedulePage {
config: Arc<AppConfig>,
schedule_manager: Arc<ScheduleManager>,
schedules: Vec<BackupSchedule>,
new_schedule_name: String,
new_schedule_source: String,
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,
show_add_schedule_dialog: bool,
file_dialog: FileDialog,
folder_selection_mode: Option<FolderSelectionMode>,
pub show_disabled_schedules: bool,
viewing_schedule_details: Option<Uuid>,
last_refresh: Option<Instant>,
}
impl SchedulePage {
pub fn new(config: Arc<AppConfig>, schedule_manager: Arc<ScheduleManager>) -> Self {
Self {
config,
schedule_manager,
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,
show_add_schedule_dialog: false,
file_dialog: FileDialog::new(),
folder_selection_mode: None,
show_disabled_schedules: true,
viewing_schedule_details: None,
last_refresh: None,
}
}
fn load_schedules(&mut self) {
match block_on(self.schedule_manager.get_all_schedules()) {
Ok(schedules) => self.schedules = schedules,
Err(e) => eprintln!("Failed to load schedules: {:?}", e),
}
}
pub fn update(&mut self, ctx: &egui::Context) {
// 檢查是否需要刷新
let should_refresh = match self.last_refresh {
None => true, // 第一次載入
Some(last) => last.elapsed() > Duration::from_millis(self.config.internal_timestamp as u64),
};
if should_refresh {
self.load_schedules();
self.last_refresh = Some(Instant::now());
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Backup Schedules");
ui.horizontal(|ui| {
if ui.button("🔄 Refresh").clicked() {
self.load_schedules();
self.last_refresh = Some(Instant::now());
}
if ui.button(" Add Schedule").clicked() {
self.show_add_schedule_dialog = true;
}
ui.separator();
let active_count = self.schedules.iter().filter(|s| s.state == ScheduleState::Active).count();
ui.label(format!("Active: {}", active_count));
let paused_count = self.schedules.iter().filter(|s| s.state == ScheduleState::Paused).count();
ui.label(format!("Paused: {}", paused_count));
let disabled_count = self.schedules.iter().filter(|s| s.state == ScheduleState::Disabled).count();
ui.label(format!("Disabled: {}", disabled_count));
});
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
let schedules_to_show: Vec<BackupSchedule> = self
.schedules
.iter()
.filter(|schedule| {
self.show_disabled_schedules || schedule.state != ScheduleState::Disabled
})
.cloned()
.collect();
for schedule in schedules_to_show {
self.draw_schedule_item(ui, &schedule);
ui.separator();
}
if self.schedules.is_empty() {
ui.vertical_centered(|ui| {
ui.label("⏰ No backup schedules");
ui.label("Click the button above to add a schedule");
});
}
});
});
self.draw_add_schedule_dialog(ctx);
self.draw_schedule_details_window(ctx);
}
fn draw_schedule_item(&mut self, ui: &mut egui::Ui, schedule: &BackupSchedule) {
egui::Frame::new()
.fill(ui.visuals().faint_bg_color)
.inner_margin(8.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
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.horizontal(|ui| {
let (color, symbol, status_text) = match schedule.state {
ScheduleState::Active => (egui::Color32::GREEN, "", "Active"),
ScheduleState::Paused => (egui::Color32::YELLOW, "⏸️", "Paused"),
ScheduleState::Disabled => (egui::Color32::GRAY, "", "Disabled"),
};
ui.colored_label(color, format!("{} {}", symbol, status_text));
if let Some(last_run) = schedule.last_run_time {
ui.separator();
ui.label(format!("Last run: {}", last_run.format("%Y-%m-%d %H:%M")));
}
if let Some(next_run) = schedule.next_run_time {
ui.separator();
ui.label(format!("Next run: {}", next_run.format("%Y-%m-%d %H:%M")));
}
});
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.small_button("👁 Details").clicked() {
self.viewing_schedule_details = Some(schedule.uuid);
}
ui.separator();
match schedule.state {
ScheduleState::Active => {
if ui.button("⏸️ Pause").clicked() {
if let Err(e) = block_on(self.schedule_manager.pause_schedule(schedule.uuid)) {
eprintln!("Failed to pause schedule: {:?}", e);
}
}
}
ScheduleState::Paused => {
if ui.button("▶️ Resume").clicked() {
if let Err(e) = block_on(self.schedule_manager.active_schedule(schedule.uuid)) {
eprintln!("Failed to resume schedule: {:?}", e);
}
}
}
ScheduleState::Disabled => {
if ui.button("▶️ Enable").clicked() {
if let Err(e) = block_on(self.schedule_manager.active_schedule(schedule.uuid)) {
eprintln!("Failed to enable schedule: {:?}", e);
}
}
}
}
if schedule.state != ScheduleState::Disabled {
if ui.button("❌ Disable").clicked() {
if let Err(e) = block_on(self.schedule_manager.disable_schedule(schedule.uuid)) {
eprintln!("Failed to disable schedule: {:?}", e);
}
}
}
if ui.button("🗑️").clicked() {
if let Err(e) = block_on(self.schedule_manager.remove_schedule(schedule.uuid)) {
eprintln!("Failed to remove schedule: {:?}", e);
}
}
});
});
});
}
fn draw_add_schedule_dialog(&mut self, ctx: &egui::Context) {
if self.show_add_schedule_dialog {
egui::Window::new("Add Backup Schedule")
.collapsible(false)
.resizable(false)
.show(ctx, |ui| {
egui::Grid::new("add_schedule_grid")
.num_columns(3)
.spacing([10.0, 4.0])
.show(ui, |ui| {
ui.label("Schedule Name:");
ui.text_edit_singleline(&mut self.new_schedule_name);
ui.label("");
ui.end_row();
ui.label("Interval:");
egui::ComboBox::from_label("")
.selected_text(format!("{:?}", self.new_schedule_interval))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.new_schedule_interval, ScheduleInterval::Once, "Once");
ui.selectable_value(&mut self.new_schedule_interval, ScheduleInterval::Daily, "Daily");
ui.selectable_value(&mut self.new_schedule_interval, ScheduleInterval::Weekly, "Weekly");
ui.selectable_value(&mut self.new_schedule_interval, ScheduleInterval::Monthly, "Monthly");
});
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.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");
ui.separator();
ui.horizontal(|ui| {
if ui.button("Create Schedule").clicked() {
if !self.new_schedule_name.is_empty()
&& !self.new_schedule_source.is_empty()
&& !self.new_schedule_destination.is_empty() {
let schedule = BackupSchedule {
uuid: Uuid::new_v4(),
name: self.new_schedule_name.clone(),
state: ScheduleState::Active,
source_path: PathBuf::from(&self.new_schedule_source),
destination_path: PathBuf::from(&self.new_schedule_destination),
backup_type: BackupType::Full,
comparison_mode: None,
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,
},
interval: self.new_schedule_interval,
last_run_time: None,
next_run_time: None,
created_at: chrono::Utc::now().naive_utc(),
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);
}
self.reset_schedule_form();
}
}
if ui.button("Cancel").clicked() {
self.show_add_schedule_dialog = false;
}
});
});
}
self.file_dialog.update(ctx);
if let Some(path) = self.file_dialog.take_picked() {
if let Some(mode) = &self.folder_selection_mode {
match mode {
FolderSelectionMode::Source => {
self.new_schedule_source = path.to_string_lossy().to_string();
}
FolderSelectionMode::Destination => {
self.new_schedule_destination = path.to_string_lossy().to_string();
}
}
}
self.folder_selection_mode = None;
}
}
fn draw_schedule_details_window(&mut self, ctx: &egui::Context) {
if let Some(schedule_id) = self.viewing_schedule_details {
let mut show_window = true;
if let Some(schedule) = self.schedules.iter().find(|s| s.uuid == schedule_id) {
egui::Window::new(format!("Schedule Details - {}", schedule.name))
.open(&mut show_window)
.resizable(true)
.default_width(500.0)
.default_height(350.0)
.show(ctx, |ui| {
egui::Grid::new("schedule_details_grid")
.num_columns(2)
.spacing([10.0, 4.0])
.show(ui, |ui| {
ui.label("Name:");
ui.label(&schedule.name);
ui.end_row();
ui.label("State:");
ui.label(format!("{:?}", schedule.state));
ui.end_row();
ui.label("Source:");
ui.label(schedule.source_path.display().to_string());
ui.end_row();
ui.label("Destination:");
ui.label(schedule.destination_path.display().to_string());
ui.end_row();
ui.label("Backup Type:");
ui.label(format!("{:?}", schedule.backup_type));
ui.end_row();
ui.label("Interval:");
ui.label(format!("{:?}", schedule.interval));
ui.end_row();
if let Some(last_run) = schedule.last_run_time {
ui.label("Last Run:");
ui.label(last_run.format("%Y-%m-%d %H:%M:%S").to_string());
ui.end_row();
}
if let Some(next_run) = schedule.next_run_time {
ui.label("Next Run:");
ui.label(next_run.format("%Y-%m-%d %H:%M:%S").to_string());
ui.end_row();
}
ui.label("Created:");
ui.label(schedule.created_at.format("%Y-%m-%d %H:%M:%S").to_string());
ui.end_row();
ui.label("Updated:");
ui.label(schedule.updated_at.format("%Y-%m-%d %H:%M:%S").to_string());
ui.end_row();
});
ui.separator();
ui.label("Options:");
ui.horizontal_wrapped(|ui| {
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");
}
if schedule.options.follow_symlinks {
ui.label("✅ Follow Symlinks");
}
});
ui.separator();
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);
}
});
});
}
if !show_window {
self.viewing_schedule_details = None;
}
}
}
fn reset_schedule_form(&mut self) {
self.new_schedule_name.clear();
self.new_schedule_source.clear();
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.show_add_schedule_dialog = false;
}
}

View File

@ -0,0 +1,29 @@
use crate::model::error::database::DatabaseError;
use crate::model::error::Error;
use crate::platform::constants::DATABASE_LOCK_PATH;
use std::fs;
use tokio::fs::File;
pub struct DatabaseLock {
_private: (),
}
impl DatabaseLock {
pub async fn acquire() -> Result<Self, Error> {
let lock = Self { _private: () };
if tokio::fs::metadata(DATABASE_LOCK_PATH).await.is_err() {
File::create(&DATABASE_LOCK_PATH)
.await
.map_err(|err| DatabaseError::LockDatabaseFailed(err))?;
Ok(lock)
} else {
Err(DatabaseError::LockDatabaseFailed("Lock file already exists."))?
}
}
}
impl Drop for DatabaseLock {
fn drop(&mut self) {
let _ = fs::remove_file(&DATABASE_LOCK_PATH);
}
}

View File

@ -1,46 +1,50 @@
use crate::model::error::io::IOError;
use crate::model::error::Error;
use blake2::{Blake2b512, Blake2s256};
use digest::{Digest, DynDigest, HashMarker};
use md5::Md5;
use sha2::Sha256;
use std::fs::File;
use std::io::Read;
use blake2::{Blake2b512, Blake2s256};
use sha2::Sha256;
use crate::utils::log_entry::io::IOEntry;
use std::path::PathBuf;
pub fn md5(file: File) -> anyhow::Result<Vec<u8>> {
pub fn md5(path: PathBuf) -> Result<Vec<u8>, Error> {
let hasher = Md5::new();
file_hash(file, hasher)
file_hash(path, hasher)
}
pub fn sha3(file: File) -> anyhow::Result<Vec<u8>> {
pub fn sha3(path: PathBuf) -> Result<Vec<u8>, Error> {
let hasher = sha3::Sha3_256::new();
file_hash(file, hasher)
file_hash(path, hasher)
}
pub fn sha256(file: File) -> anyhow::Result<Vec<u8>> {
pub fn sha256(path: PathBuf) -> Result<Vec<u8>, Error> {
let hasher = Sha256::new();
file_hash(file, hasher)
file_hash(path, hasher)
}
pub fn blake2b(file: File) -> anyhow::Result<Vec<u8>> {
pub fn blake2b(path: PathBuf) -> Result<Vec<u8>, Error> {
let hasher = Blake2b512::new();
file_hash(file, hasher)
file_hash(path, hasher)
}
pub fn blake2s(file: File) -> anyhow::Result<Vec<u8>> {
pub fn blake2s(path: PathBuf) -> Result<Vec<u8>, Error> {
let hasher = Blake2s256::new();
file_hash(file, hasher)
file_hash(path, hasher)
}
pub fn blake3(file: File) -> anyhow::Result<Vec<u8>> {
pub fn blake3(path: PathBuf) -> Result<Vec<u8>, Error> {
let hasher = blake3::Hasher::new();
file_hash(file, hasher)
file_hash(path, hasher)
}
fn file_hash(mut file: File, mut hasher: impl HashMarker + DynDigest) -> anyhow::Result<Vec<u8>> {
fn file_hash(path: PathBuf, mut hasher: impl HashMarker + DynDigest) -> Result<Vec<u8>, Error> {
let mut file = File::open(&path).map_err(|err| IOError::ReadFileFailed(path.clone(), err))?;
let mut buffer = [0; 65536];
loop {
let bytes_read = file.read(&mut buffer)
.map_err(|_| IOEntry::ReadFileFailed)?;
let bytes_read = file
.read(&mut buffer)
.map_err(|err| IOError::ReadFileFailed(path.clone(), err))?;
if bytes_read == 0 {
break;
}

View File

@ -1,28 +1,35 @@
use tokio::fs::File;
use std::io;
use std::path::Path;
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<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = File::open(path).await?;
file.try_lock_exclusive()?;
Ok(Self { file })
}
pub async fn from_file(file: File) -> io::Result<Self> {
file.try_lock_exclusive()?;
Ok(Self { file })
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 _ = self.file.unlock();
let path = self.path.clone();
if let Err(err) = self.file.unlock() {
log!(IOError::UnlockFileFailed(path, err));
}
}
}

View File

@ -1,20 +0,0 @@
use crate::utils::log_entry::define_log_entries;
define_log_entries! {
DatabaseEntry {
#[error("Failed to create database")]
CreateDatabaseFailed: tracing::Level::ERROR,
#[error("Connected to database successfully")]
DatabaseConnectSuccess: tracing::Level::INFO,
#[error("Failed to connect to database")]
DatabaseConnectFailed: tracing::Level::ERROR,
#[error("Failed to lock database")]
LockDatabaseFailed: tracing::Level::ERROR,
#[error("Failed to unlock database")]
UnlockDatabaseFailed: tracing::Level::ERROR,
}
}

View File

@ -1,5 +0,0 @@
use thiserror::Error;
use crate::define_log_entries;
#[derive(Error, Debug)]
pub enum EventEntry {}

View File

@ -1,32 +0,0 @@
use crate::define_log_entries;
define_log_entries! {
IOEntry {
#[error("Semaphore has been closed")]
SemaphoreClosed: tracing::Level::ERROR,
#[error("Failed to create directory")]
CreateDirectoryFailed: tracing::Level::ERROR,
#[error("Failed to read directory")]
ReadDirectoryFailed: tracing::Level::ERROR,
#[error("Failed to read file")]
ReadFileFailed: tracing::Level::ERROR,
#[error("Failed to copy file")]
CopyFileFailed: tracing::Level::ERROR,
#[error("Failed to delete directory")]
DeleteDirectoryFailed: tracing::Level::ERROR,
#[error("Failed to delete file")]
DeleteFileFailed: tracing::Level::ERROR,
#[error("Failed to get file metadata")]
GetMetadataFailed: tracing::Level::ERROR,
#[error("Failed to set file metadata")]
SetMetadataFailed: tracing::Level::ERROR,
}
}

View File

@ -1,6 +0,0 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MiscEntry {
}

View File

@ -1,70 +0,0 @@
pub mod database;
pub mod event;
pub mod io;
pub mod misc;
pub mod system;
pub mod task;
pub use crate::define_log_entries;
#[macro_export]
macro_rules! define_log_entries {
(
$enum_name:ident {
$(
$(#[doc = $doc:expr])*
#[error($msg:expr)]
$variant:ident: $level:expr
$(,)?
)*
}
) => {
#[allow(dead_code)]
#[derive(thiserror::Error, Debug)]
pub enum $enum_name {
$(
$(#[doc = $doc])*
#[error($msg)]
$variant,
)*
}
impl $enum_name {
#[allow(dead_code)]
pub fn level(&self) -> tracing::Level {
match self {
$(Self::$variant => $level,)*
}
}
#[allow(dead_code)]
pub fn log(&self) {
let level = self.level();
let message = self.to_string();
match level {
tracing::Level::ERROR => tracing::error!("{}", message),
tracing::Level::WARN => tracing::warn!("{}", message),
tracing::Level::INFO => tracing::info!("{}", message),
tracing::Level::DEBUG => tracing::debug!("{}", message),
tracing::Level::TRACE => tracing::trace!("{}", message),
}
}
#[allow(dead_code)]
pub fn log_with_context<T: std::fmt::Display>(&self, context: T) {
let level = self.level();
let message = self.to_string();
let context = context.to_string();
match level {
tracing::Level::ERROR => tracing::error!(message = %message, context = %context),
tracing::Level::WARN => tracing::warn!(message = %message, context = %context),
tracing::Level::INFO => tracing::info!(message = %message, context = %context),
tracing::Level::DEBUG => tracing::debug!(message = %message, context = %context),
tracing::Level::TRACE => tracing::trace!(message = %message, context = %context),
}
}
}
};
}

View File

@ -1,47 +0,0 @@
use crate::define_log_entries;
define_log_entries! {
SystemEntry {
#[error("Rerun the program as administrator")]
ReRunAsAdmin: tracing::Level::WARN,
#[error("Unable to run as administrator")]
RunAsAdminFailed: tracing::Level::ERROR,
#[error("Failed to adjust token privileges")]
AdjustTokenPrivilegesFailed: tracing::Level::ERROR,
#[error("Failed to free object")]
ObjectFreeFailed: tracing::Level::ERROR,
#[error("Online now")]
Online: tracing::Level::INFO,
#[error("Initializing")]
Initializing: tracing::Level::INFO,
#[error("Initialization completed")]
InitializeComplete: tracing::Level::INFO,
#[error("Termination in process")]
Terminating: tracing::Level::INFO,
#[error("Termination completed")]
TerminateComplete: tracing::Level::INFO,
#[error("Invalid configuration")]
InvalidConfig: tracing::Level::ERROR,
#[error("Configuration not found")]
ConfigNotFound: tracing::Level::ERROR,
#[error("Internal error")]
InternalError: tracing::Level::ERROR,
#[error("Unknown error")]
ThreadPanic: tracing::Level::ERROR,
#[error("Unknown error")]
UnknownError: tracing::Level::ERROR,
}
}

View File

@ -1,8 +0,0 @@
use crate::define_log_entries;
define_log_entries! {
TaskEntry {
#[error("Task not found")]
TaskNotFound: tracing::Level::ERROR,
}
}

View File

@ -1,4 +1,4 @@
pub mod database_lock;
pub mod file_hash;
pub mod file_lock;
pub mod log_entry;
pub mod logging;