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:
parent
4bbb0e1d3d
commit
c14fd32277
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
/target
|
||||
/logs
|
||||
target
|
||||
logs
|
||||
.idea
|
||||
mirrorSphere.db
|
||||
db.lock
|
||||
|
||||
3297
Cargo.lock
generated
3297
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
64
Cargo.toml
64
Cargo.toml
@ -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"
|
||||
]
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
[Config]
|
||||
internal_timestamp = 20
|
||||
default_wakeup_time = 10
|
||||
max_concurrency = 16
|
||||
max_file_operations = 100
|
||||
|
||||
12
devnote.txt
12
devnote.txt
@ -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
12
macros/Cargo.toml
Normal 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
20
macros/src/lib.rs
Normal 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
66
macros/src/log.rs
Normal 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
143
macros/src/loggable.rs
Normal 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
231
macros/src/traceable.rs
Normal 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)
|
||||
}
|
||||
@ -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
689
src/core/backup_engine.rs
Normal 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(¤t_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(
|
||||
¤t_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(¤t_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, ¤t_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, ¤t_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
43
src/core/event_bus.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
62
src/core/gui_manager.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
308
src/core/schedule_manager.rs
Normal file
308
src/core/schedule_manager.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
pub trait Actor: Send + Sync + 'static {}
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
pub mod actor;
|
||||
pub mod dispatcher;
|
||||
pub mod event;
|
||||
pub mod event_handler;
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
1
src/interface/repository/mod.rs
Normal file
1
src/interface/repository/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod schedule;
|
||||
295
src/interface/repository/schedule.rs
Normal file
295
src/interface/repository/schedule.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
16
src/interface/service_unit.rs
Normal file
16
src/interface/service_unit.rs
Normal 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<()>);
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
55
src/model/backup/backup_schedule.rs
Normal file
55
src/model/backup/backup_schedule.rs
Normal 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
3
src/model/backup/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod backup_execution;
|
||||
pub mod backup_schedule;
|
||||
pub mod progress_data;
|
||||
18
src/model/backup/progress_data.rs
Normal file
18
src/model/backup/progress_data.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
24
src/model/error/database.rs
Normal file
24
src/model/error/database.rs
Normal 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
8
src/model/error/event.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
EventError {
|
||||
#[error("Placeholder")]
|
||||
Placeholder => tracing::Level::INFO,
|
||||
}
|
||||
}
|
||||
55
src/model/error/io.rs
Normal file
55
src/model/error/io.rs
Normal 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
17
src/model/error/misc.rs
Normal 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
66
src/model/error/mod.rs
Normal 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
35
src/model/error/system.rs
Normal 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
16
src/model/error/task.rs
Normal 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
12
src/model/event/error.rs
Normal 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
|
||||
48
src/model/event/execution.rs
Normal file
48
src/model/event/execution.rs
Normal 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 {}
|
||||
10
src/model/event/filesystem.rs
Normal file
10
src/model/event/filesystem.rs
Normal 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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -1,5 +0,0 @@
|
||||
pub mod attributes;
|
||||
pub mod directory;
|
||||
pub mod file;
|
||||
pub mod hash;
|
||||
pub mod permission;
|
||||
@ -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 {}
|
||||
@ -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;
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
use crate::interface::event_system::event::Event;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SaveCheckPointEvent {}
|
||||
|
||||
impl Event for SaveCheckPointEvent {}
|
||||
@ -1,2 +0,0 @@
|
||||
pub mod checkpoint;
|
||||
pub mod task;
|
||||
@ -1,6 +0,0 @@
|
||||
use crate::interface::event_system::event::Event;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UpdateProgressEvent {}
|
||||
|
||||
impl Event for UpdateProgressEvent {}
|
||||
39
src/model/event/schedule.rs
Normal file
39
src/model/event/schedule.rs
Normal 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 {}
|
||||
@ -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 {}
|
||||
8
src/model/log/database.rs
Normal file
8
src/model/log/database.rs
Normal 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
8
src/model/log/event.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use macros::loggable;
|
||||
|
||||
loggable! {
|
||||
EventLog {
|
||||
#[error("Placeholder")]
|
||||
Placeholder => tracing::Level::INFO,
|
||||
}
|
||||
}
|
||||
8
src/model/log/io.rs
Normal file
8
src/model/log/io.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use macros::loggable;
|
||||
|
||||
loggable! {
|
||||
IOLog {
|
||||
#[error("Placeholder")]
|
||||
Placeholder => tracing::Level::INFO,
|
||||
}
|
||||
}
|
||||
8
src/model/log/misc.rs
Normal file
8
src/model/log/misc.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use macros::loggable;
|
||||
|
||||
loggable! {
|
||||
MiscLog {
|
||||
#[error("Placeholder")]
|
||||
Placeholder => tracing::Level::INFO,
|
||||
}
|
||||
}
|
||||
6
src/model/log/mod.rs
Normal file
6
src/model/log/mod.rs
Normal 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
23
src/model/log/system.rs
Normal 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
8
src/model/log/task.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use macros::loggable;
|
||||
|
||||
loggable! {
|
||||
TaskLog {
|
||||
#[error("Placeholder")]
|
||||
Placeholder => tracing::Level::INFO,
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
pub mod task;
|
||||
pub mod backup;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod log;
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
pub mod attributes;
|
||||
pub mod constants;
|
||||
pub mod database_ops;
|
||||
pub mod elevate;
|
||||
pub mod file_system;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
454
src/ui/execution_page.rs
Normal 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
89
src/ui/main_page.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
pub mod execution_page;
|
||||
pub mod main_page;
|
||||
pub mod schedule_page;
|
||||
449
src/ui/schedule_page.rs
Normal file
449
src/ui/schedule_page.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/utils/database_lock.rs
Normal file
29
src/utils/database_lock.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
use thiserror::Error;
|
||||
use crate::define_log_entries;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EventEntry {}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MiscEntry {
|
||||
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
use crate::define_log_entries;
|
||||
|
||||
define_log_entries! {
|
||||
TaskEntry {
|
||||
#[error("Task not found")]
|
||||
TaskNotFound: tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
pub mod database_lock;
|
||||
pub mod file_hash;
|
||||
pub mod file_lock;
|
||||
pub mod log_entry;
|
||||
pub mod logging;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user