use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ProjectState {
Local,
Archived,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMeta {
pub version: u32,
pub uuid: Uuid,
pub name: String,
pub local_path: String,
pub remote_path: String,
pub size_bytes: u64,
#[serde(default)]
pub file_count: u32,
pub last_sync: DateTime<Utc>,
pub compression: bool,
#[serde(default)]
pub encryption_enabled: bool,
#[serde(default)]
pub public_key: Option<String>,
pub state: ProjectState,
}
impl ProjectMeta {
pub fn new_local(name: String, local_path: String, remote_path: String) -> Self {
Self {
version: 1,
uuid: Uuid::new_v4(),
name,
local_path,
remote_path,
size_bytes: 0,
file_count: 0,
last_sync: Utc::now(),
compression: false,
encryption_enabled: false,
public_key: None,
state: ProjectState::Local,
}
}
pub fn new_local_encrypted(
name: String,
local_path: String,
remote_path: String,
public_key: String,
) -> Self {
Self {
version: 1,
uuid: Uuid::new_v4(),
name,
local_path,
remote_path,
size_bytes: 0,
file_count: 0,
last_sync: Utc::now(),
compression: false,
encryption_enabled: true,
public_key: Some(public_key),
state: ProjectState::Local,
}
}
pub fn mark_archived(&mut self, size_bytes: u64, file_count: u32, compression: bool) {
self.state = ProjectState::Archived;
self.size_bytes = size_bytes;
self.file_count = file_count;
self.compression = compression;
self.last_sync = Utc::now();
}
pub fn mark_local(&mut self, size_bytes: u64, file_count: u32) {
self.state = ProjectState::Local;
self.size_bytes = size_bytes;
self.file_count = file_count;
self.last_sync = Utc::now();
}
pub fn is_archived(&self) -> bool {
self.state == ProjectState::Archived
}
pub fn is_local(&self) -> bool {
self.state == ProjectState::Local
}
pub fn days_since_sync(&self) -> f64 {
let duration = Utc::now() - self.last_sync;
duration.num_milliseconds() as f64 / (1000.0 * 60.0 * 60.0 * 24.0)
}
pub fn size_human(&self) -> String {
human_size(self.size_bytes)
}
}
pub fn human_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = bytes as f64;
let mut unit_idx = 0;
while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
size /= 1024.0;
unit_idx += 1;
}
if unit_idx == 0 {
format!("{} B", bytes)
} else {
format!("{:.1} {}", size, UNITS[unit_idx])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_human_size() {
assert_eq!(human_size(0), "0 B");
assert_eq!(human_size(1023), "1023 B");
assert_eq!(human_size(1024), "1.0 KB");
assert_eq!(human_size(1_048_576), "1.0 MB");
assert_eq!(human_size(1_073_741_824), "1.0 GB");
assert_eq!(human_size(2_199_023_255_552), "2.0 TB");
}
#[test]
fn test_project_state_serialization() {
let meta = ProjectMeta::new_local(
"testproj".into(),
"/home/jacob/Projects/testproj".into(),
"nas.local:9742:/srv/pwr/projects/testproj".into(),
);
assert_eq!(meta.state, ProjectState::Local);
assert_eq!(meta.version, 1);
assert!(!meta.is_archived());
assert!(meta.is_local());
}
#[test]
fn test_state_transitions() {
let mut meta = ProjectMeta::new_local(
"testproj".into(),
"/home/jacob/Projects/testproj".into(),
"nas.local:9742:/srv/pwr/projects/testproj".into(),
);
assert!(meta.is_local());
meta.mark_archived(1_048_576, 42, true);
assert!(meta.is_archived());
assert_eq!(meta.size_bytes, 1_048_576);
assert_eq!(meta.file_count, 42);
assert!(meta.compression);
meta.mark_local(1_048_576, 42);
assert!(meta.is_local());
assert!(!meta.is_archived());
}
#[test]
fn test_encrypted_project() {
let meta = ProjectMeta::new_local_encrypted(
"secretproj".into(),
"/home/jacob/Projects/secretproj".into(),
"nas.local:9742:/srv/pwr/projects/secretproj".into(),
"age1qx0...".into(),
);
assert!(meta.encryption_enabled);
assert!(meta.public_key.is_some());
}
#[test]
fn test_days_since_sync() {
let meta = ProjectMeta::new_local(
"recent".into(),
"/tmp/recent".into(),
"nas.local:9742:/srv/pwr/projects/recent".into(),
);
assert!(meta.days_since_sync() < 0.01);
}
#[test]
fn test_serialization_round_trip() {
let meta = ProjectMeta::new_local_encrypted(
"roundtrip".into(),
"/tmp/roundtrip".into(),
"server:9742:/srv/roundtrip".into(),
"age1test".into(),
);
let toml_str = toml::to_string_pretty(&meta).unwrap();
let parsed: ProjectMeta = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.uuid, meta.uuid);
assert_eq!(parsed.name, "roundtrip");
assert_eq!(parsed.encryption_enabled, true);
assert_eq!(parsed.public_key, Some("age1test".into()));
assert_eq!(parsed.state, ProjectState::Local);
}
}