use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileEntry {
pub path: PathBuf,
pub content_hash: String,
pub size: u64,
pub permissions: u32,
pub modified: DateTime<Utc>,
pub is_compressed: bool,
pub metadata_hash: String,
pub combined_hash: String,
pub is_symlink: bool,
pub symlink_target: Option<PathBuf>,
pub is_directory: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileManifest {
pub checkpoint_id: String,
pub files: Vec<FileEntry>,
pub total_size: u64,
pub file_count: usize,
pub merkle_root: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChangeStats {
pub files_added: usize,
pub files_modified: usize,
pub files_deleted: usize,
pub bytes_added: u64,
pub bytes_modified: u64,
pub bytes_deleted: u64,
pub changed_files: Vec<PathBuf>,
}
impl ChangeStats {
pub fn has_changes(&self) -> bool {
self.files_added > 0 || self.files_modified > 0 || self.files_deleted > 0
}
pub fn total_operations(&self) -> usize {
self.files_added + self.files_modified + self.files_deleted
}
pub fn net_size_change(&self) -> i64 {
(self.bytes_added + self.bytes_modified) as i64 - self.bytes_deleted as i64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreResult {
pub checkpoint_id: String,
pub files_restored: usize,
pub files_deleted: usize,
pub bytes_written: u64,
pub bytes_deleted: u64,
pub duration_ms: u64,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointDiff {
pub from_id: String,
pub to_id: String,
pub added_files: Vec<FileEntry>,
pub modified_files: Vec<(FileEntry, FileEntry)>,
pub deleted_files: Vec<FileEntry>,
pub stats: ChangeStats,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum LineChange {
Added(usize, String),
Deleted(usize, String),
Context(usize, String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffHunk {
pub from_line: usize,
pub from_count: usize,
pub to_line: usize,
pub to_count: usize,
pub changes: Vec<LineChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDiff {
pub path: PathBuf,
pub from_hash: String,
pub to_hash: String,
pub is_binary: bool,
pub hunks: Vec<DiffHunk>,
pub lines_added: usize,
pub lines_deleted: usize,
}
#[derive(Debug, Clone)]
pub struct DiffOptions {
pub context_lines: usize,
pub ignore_whitespace: bool,
pub show_line_numbers: bool,
pub max_file_size: u64,
}
impl Default for DiffOptions {
fn default() -> Self {
Self {
context_lines: 3,
ignore_whitespace: false,
show_line_numbers: true,
max_file_size: 10 * 1024 * 1024, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetailedCheckpointDiff {
pub basic_diff: CheckpointDiff,
pub file_diffs: Vec<FileDiff>,
pub total_lines_added: usize,
pub total_lines_deleted: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GcStats {
pub objects_examined: usize,
pub objects_deleted: usize,
pub bytes_reclaimed: u64,
pub duration_ms: u64,
pub unreferenced_objects: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TitorConfig {
pub root_path: PathBuf,
pub storage_path: PathBuf,
pub max_file_size: u64,
pub parallel_workers: usize,
pub ignore_patterns: Vec<String>,
pub compression_strategy: String,
pub follow_symlinks: bool,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageMetadata {
pub format_version: u32,
pub titor_version: String,
pub created_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
pub config: TitorConfig,
}
pub type ProgressCallback = Arc<dyn Fn(ProgressInfo) + Send + Sync>;
#[derive(Debug, Clone)]
pub struct ProgressInfo {
pub operation: String,
pub current_item: Option<String>,
pub processed: usize,
pub total: Option<usize>,
pub bytes_processed: u64,
pub total_bytes: Option<u64>,
}
impl ProgressInfo {
pub fn percentage(&self) -> Option<f32> {
match self.total {
Some(total) if total > 0 => Some((self.processed as f32 / total as f32) * 100.0),
_ => None,
}
}
}
#[derive(Clone)]
pub enum AutoCheckpointStrategy {
Disabled,
AfterOperations(usize),
TimeBased(std::time::Duration),
Smart {
min_files_changed: usize,
min_size_changed: u64,
max_time_between: std::time::Duration,
},
Custom(Arc<dyn Fn(&ChangeStats) -> bool + Send + Sync>),
}
impl std::fmt::Debug for AutoCheckpointStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled => write!(f, "Disabled"),
Self::AfterOperations(n) => f.debug_tuple("AfterOperations").field(n).finish(),
Self::TimeBased(duration) => f.debug_tuple("TimeBased").field(duration).finish(),
Self::Smart { min_files_changed, min_size_changed, max_time_between } => f
.debug_struct("Smart")
.field("min_files_changed", min_files_changed)
.field("min_size_changed", min_size_changed)
.field("max_time_between", max_time_between)
.finish(),
Self::Custom(_) => write!(f, "Custom(Fn)"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileOperation {
Added(PathBuf),
Modified(PathBuf),
Deleted(PathBuf),
}
#[derive(Clone, Default)]
pub struct CheckpointOptions {
pub description: Option<String>,
pub tags: Vec<String>,
pub metadata: HashMap<String, String>,
pub progress_callback: Option<ProgressCallback>,
}
impl std::fmt::Debug for CheckpointOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckpointOptions")
.field("description", &self.description)
.field("tags", &self.tags)
.field("metadata", &self.metadata)
.field("progress_callback", &self.progress_callback.is_some())
.finish()
}
}
#[derive(Clone, Default)]
pub struct RestoreOptions {
pub verify_hashes: bool,
pub preserve_timestamps: bool,
pub exclude_patterns: Vec<String>,
pub progress_callback: Option<ProgressCallback>,
pub dry_run: bool,
}
impl std::fmt::Debug for RestoreOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RestoreOptions")
.field("verify_hashes", &self.verify_hashes)
.field("preserve_timestamps", &self.preserve_timestamps)
.field("exclude_patterns", &self.exclude_patterns)
.field("progress_callback", &self.progress_callback.is_some())
.field("dry_run", &self.dry_run)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageObject {
pub hash: String,
pub compressed_size: u64,
pub uncompressed_size: u64,
pub ref_count: usize,
pub is_compressed: bool,
pub created_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
}
pub type FileFilter = Arc<dyn Fn(&FileEntry) -> bool + Send + Sync>;
pub trait CheckpointHook: Send + Sync {
fn pre_checkpoint(&self, stats: &ChangeStats) -> crate::error::Result<()>;
fn post_checkpoint(&self, checkpoint: &crate::checkpoint::Checkpoint) -> crate::error::Result<()>;
fn pre_restore(&self, from: &crate::checkpoint::Checkpoint, to: &crate::checkpoint::Checkpoint) -> crate::error::Result<()>;
fn post_restore(&self, result: &RestoreResult) -> crate::error::Result<()>;
}
#[derive(Debug)]
pub struct NoOpHook;
impl CheckpointHook for NoOpHook {
fn pre_checkpoint(&self, _stats: &ChangeStats) -> crate::error::Result<()> {
Ok(())
}
fn post_checkpoint(&self, _checkpoint: &crate::checkpoint::Checkpoint) -> crate::error::Result<()> {
Ok(())
}
fn pre_restore(&self, _from: &crate::checkpoint::Checkpoint, _to: &crate::checkpoint::Checkpoint) -> crate::error::Result<()> {
Ok(())
}
fn post_restore(&self, _result: &RestoreResult) -> crate::error::Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineState {
pub current_checkpoint_id: Option<String>,
pub version: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_change_stats() {
let mut stats = ChangeStats::default();
assert!(!stats.has_changes());
stats.files_added = 5;
stats.bytes_added = 1000;
assert!(stats.has_changes());
assert_eq!(stats.total_operations(), 5);
assert_eq!(stats.net_size_change(), 1000);
}
#[test]
fn test_progress_info() {
let info = ProgressInfo {
operation: "Scanning".to_string(),
current_item: None,
processed: 50,
total: Some(100),
bytes_processed: 0,
total_bytes: None,
};
assert_eq!(info.percentage(), Some(50.0));
}
}