use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use lazy_static::lazy_static;
use spin::Mutex;
#[derive(Debug, Clone)]
pub struct FileVersion {
pub file_id: u64,
pub version: u64,
pub timestamp: u64,
pub size: u64,
pub block_ptr: u64,
pub checksum: [u8; 32],
pub user_id: u64,
pub description: &'static str,
}
impl FileVersion {
pub fn new(
file_id: u64,
version: u64,
timestamp: u64,
size: u64,
block_ptr: u64,
checksum: [u8; 32],
user_id: u64,
) -> Self {
Self {
file_id,
version,
timestamp,
size,
block_ptr,
checksum,
user_id,
description: "",
}
}
pub fn with_description(mut self, desc: &'static str) -> Self {
self.description = desc;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct VersioningPolicy {
pub max_versions: u32,
pub retention_seconds: u64,
pub min_version_interval: u64,
pub manual_only: bool,
}
impl Default for VersioningPolicy {
fn default() -> Self {
Self {
max_versions: 10, retention_seconds: 2592000, min_version_interval: 300, manual_only: false,
}
}
}
#[derive(Debug, Clone)]
pub struct VersionHistory {
pub file_id: u64,
pub policy: VersioningPolicy,
versions: Vec<FileVersion>,
last_version_time: u64,
}
impl VersionHistory {
pub fn new(file_id: u64, policy: VersioningPolicy) -> Self {
Self {
file_id,
policy,
versions: Vec::new(),
last_version_time: 0,
}
}
pub fn should_create_version(&self, current_time: u64) -> bool {
if self.policy.manual_only {
return false; }
let elapsed = current_time.saturating_sub(self.last_version_time);
elapsed >= self.policy.min_version_interval
}
pub fn add_version(&mut self, version: FileVersion) {
self.last_version_time = version.timestamp;
self.versions.push(version);
if self.policy.max_versions > 0 {
let max = self.policy.max_versions as usize;
if self.versions.len() > max {
let to_remove = self.versions.len() - max;
self.versions.drain(0..to_remove);
}
}
}
pub fn prune_old_versions(&mut self, current_time: u64) {
if self.policy.retention_seconds == 0 {
return; }
let cutoff = current_time.saturating_sub(self.policy.retention_seconds);
self.versions.retain(|v| v.timestamp >= cutoff);
}
pub fn get_version(&self, version: u64) -> Option<&FileVersion> {
self.versions.iter().find(|v| v.version == version)
}
pub fn latest_version(&self) -> Option<&FileVersion> {
self.versions.last()
}
pub fn all_versions(&self) -> &[FileVersion] {
&self.versions
}
pub fn version_count(&self) -> usize {
self.versions.len()
}
pub fn total_storage(&self) -> u64 {
self.versions.iter().map(|v| v.size).sum()
}
}
#[derive(Debug, Clone)]
pub struct VersionDiff {
pub old_version: u64,
pub new_version: u64,
pub size_delta: i64,
pub time_delta: u64,
pub content_changed: bool,
}
#[derive(Debug, Clone, Default)]
pub struct VersioningStats {
pub versioned_files: usize,
pub total_versions: u64,
pub total_storage: u64,
pub versions_pruned: u64,
}
lazy_static! {
static ref VERSIONING_MANAGER: Mutex<VersioningManager> = Mutex::new(VersioningManager::new());
}
pub struct VersioningManager {
histories: BTreeMap<u64, VersionHistory>,
default_policy: VersioningPolicy,
stats: VersioningStats,
}
impl Default for VersioningManager {
fn default() -> Self {
Self::new()
}
}
impl VersioningManager {
pub fn new() -> Self {
Self {
histories: BTreeMap::new(),
default_policy: VersioningPolicy::default(),
stats: VersioningStats::default(),
}
}
pub fn enable_versioning(&mut self, file_id: u64, policy: Option<VersioningPolicy>) {
let policy = policy.unwrap_or(self.default_policy);
let history = VersionHistory::new(file_id, policy);
self.histories.insert(file_id, history);
self.stats.versioned_files += 1;
}
pub fn disable_versioning(&mut self, file_id: u64) {
if let Some(history) = self.histories.remove(&file_id) {
self.stats.versioned_files = self.stats.versioned_files.saturating_sub(1);
self.stats.total_versions = self
.stats
.total_versions
.saturating_sub(history.version_count() as u64);
}
}
pub fn create_version(
&mut self,
mut version: FileVersion,
manual: bool,
) -> Result<u64, &'static str> {
let file_id = version.file_id;
let timestamp = version.timestamp;
let history = self
.histories
.get_mut(&file_id)
.ok_or("Versioning not enabled")?;
if !manual && !history.should_create_version(timestamp) {
return Err("Too soon for automatic version");
}
let version_num = history.version_count() as u64 + 1;
version.version = version_num;
let size = version.size;
history.add_version(version);
self.stats.total_versions += 1;
self.stats.total_storage += size;
Ok(version_num)
}
pub fn get_history(&self, file_id: u64) -> Option<&VersionHistory> {
self.histories.get(&file_id)
}
pub fn get_version(&self, file_id: u64, version: u64) -> Option<&FileVersion> {
self.histories.get(&file_id)?.get_version(version)
}
pub fn get_latest(&self, file_id: u64) -> Option<&FileVersion> {
self.histories.get(&file_id)?.latest_version()
}
pub fn list_versions(&self, file_id: u64) -> Option<Vec<FileVersion>> {
self.histories
.get(&file_id)
.map(|h| h.all_versions().to_vec())
}
pub fn diff_versions(&self, file_id: u64, old_ver: u64, new_ver: u64) -> Option<VersionDiff> {
let history = self.histories.get(&file_id)?;
let old = history.get_version(old_ver)?;
let new = history.get_version(new_ver)?;
Some(VersionDiff {
old_version: old_ver,
new_version: new_ver,
size_delta: new.size as i64 - old.size as i64,
time_delta: new.timestamp.saturating_sub(old.timestamp),
content_changed: old.checksum != new.checksum,
})
}
pub fn prune_all(&mut self, current_time: u64) -> u64 {
let mut pruned = 0;
for history in self.histories.values_mut() {
let before = history.version_count();
history.prune_old_versions(current_time);
let after = history.version_count();
pruned += (before - after) as u64;
}
self.stats.versions_pruned += pruned;
self.stats.total_versions = self.stats.total_versions.saturating_sub(pruned);
pruned
}
pub fn get_stats(&self) -> VersioningStats {
let mut stats = self.stats.clone();
stats.total_storage = self.histories.values().map(|h| h.total_storage()).sum();
stats
}
pub fn set_default_policy(&mut self, policy: VersioningPolicy) {
self.default_policy = policy;
}
}
pub struct VersioningEngine;
impl VersioningEngine {
pub fn enable(file_id: u64, policy: Option<VersioningPolicy>) {
let mut mgr = VERSIONING_MANAGER.lock();
mgr.enable_versioning(file_id, policy);
}
pub fn disable(file_id: u64) {
let mut mgr = VERSIONING_MANAGER.lock();
mgr.disable_versioning(file_id);
}
pub fn create_version(
file_id: u64,
timestamp: u64,
size: u64,
block_ptr: u64,
checksum: [u8; 32],
user_id: u64,
) -> Result<u64, &'static str> {
let version = FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
let mut mgr = VERSIONING_MANAGER.lock();
mgr.create_version(version, true)
}
pub fn auto_version(
file_id: u64,
timestamp: u64,
size: u64,
block_ptr: u64,
checksum: [u8; 32],
user_id: u64,
) -> Result<u64, &'static str> {
let version = FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
let mut mgr = VERSIONING_MANAGER.lock();
mgr.create_version(version, false)
}
pub fn get_version(file_id: u64, version: u64) -> Option<FileVersion> {
let mgr = VERSIONING_MANAGER.lock();
mgr.get_version(file_id, version).cloned()
}
pub fn list_versions(file_id: u64) -> Option<Vec<FileVersion>> {
let mgr = VERSIONING_MANAGER.lock();
mgr.list_versions(file_id)
}
pub fn diff(file_id: u64, old_ver: u64, new_ver: u64) -> Option<VersionDiff> {
let mgr = VERSIONING_MANAGER.lock();
mgr.diff_versions(file_id, old_ver, new_ver)
}
pub fn prune(current_time: u64) -> u64 {
let mut mgr = VERSIONING_MANAGER.lock();
mgr.prune_all(current_time)
}
pub fn stats() -> VersioningStats {
let mgr = VERSIONING_MANAGER.lock();
mgr.get_stats()
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
impl VersioningManager {
fn test_create_version(
&mut self,
file_id: u64,
timestamp: u64,
size: u64,
block_ptr: u64,
checksum: [u8; 32],
user_id: u64,
manual: bool,
) -> Result<u64, &'static str> {
let version =
FileVersion::new(file_id, 0, timestamp, size, block_ptr, checksum, user_id);
self.create_version(version, manual)
}
}
#[test]
fn test_default_policy() {
let policy = VersioningPolicy::default();
assert_eq!(policy.max_versions, 10);
assert_eq!(policy.retention_seconds, 2592000); }
#[test]
fn test_version_creation() {
let mut history = VersionHistory::new(1, VersioningPolicy::default());
let version = FileVersion::new(1, 1, 0, 1024, 100, [0u8; 32], 999);
history.add_version(version);
assert_eq!(history.version_count(), 1);
}
#[test]
fn test_max_versions_limit() {
let mut policy = VersioningPolicy::default();
policy.max_versions = 3;
let mut history = VersionHistory::new(1, policy);
for i in 1..=5 {
let version = FileVersion::new(1, i, i * 100, 1024, 100 + i, [0u8; 32], 999);
history.add_version(version);
}
assert_eq!(history.version_count(), 3);
assert!(history.get_version(1).is_none()); assert!(history.get_version(2).is_none()); assert!(history.get_version(3).is_some()); }
#[test]
fn test_retention_pruning() {
let mut policy = VersioningPolicy::default();
policy.retention_seconds = 1000;
let mut history = VersionHistory::new(1, policy);
let old = FileVersion::new(1, 1, 0, 1024, 100, [0u8; 32], 999);
history.add_version(old);
let recent = FileVersion::new(1, 2, 2000, 1024, 101, [0u8; 32], 999);
history.add_version(recent);
history.prune_old_versions(2500);
assert_eq!(history.version_count(), 1);
assert!(history.get_version(1).is_none());
assert!(history.get_version(2).is_some());
}
#[test]
fn test_auto_version_interval() {
let mut policy = VersioningPolicy::default();
policy.min_version_interval = 300;
let history = VersionHistory::new(1, policy);
assert!(!history.should_create_version(100));
assert!(history.should_create_version(400));
}
#[test]
fn test_manual_only_mode() {
let mut policy = VersioningPolicy::default();
policy.manual_only = true;
let history = VersionHistory::new(1, policy);
assert!(!history.should_create_version(1000000));
}
#[test]
fn test_versioning_manager() {
let mut mgr = VersioningManager::new();
mgr.enable_versioning(1, None);
assert_eq!(mgr.stats.versioned_files, 1);
mgr.test_create_version(1, 0, 1024, 100, [0u8; 32], 999, true)
.expect("test: operation should succeed");
assert_eq!(mgr.stats.total_versions, 1);
let version = mgr
.get_version(1, 1)
.expect("test: operation should succeed");
assert_eq!(version.size, 1024);
}
#[test]
fn test_version_diff() {
let mut mgr = VersioningManager::new();
mgr.enable_versioning(1, None);
mgr.test_create_version(1, 0, 1024, 100, [1u8; 32], 999, true)
.expect("test: operation should succeed");
mgr.test_create_version(1, 1000, 2048, 101, [2u8; 32], 999, true)
.expect("test: operation should succeed");
let diff = mgr
.diff_versions(1, 1, 2)
.expect("test: operation should succeed");
assert_eq!(diff.size_delta, 1024); assert_eq!(diff.time_delta, 1000);
assert!(diff.content_changed);
}
#[test]
fn test_list_versions() {
let mut mgr = VersioningManager::new();
mgr.enable_versioning(1, None);
for i in 1..=5 {
mgr.test_create_version(1, i * 100, 1024, 100 + i, [0u8; 32], 999, true)
.expect("test: operation should succeed");
}
let versions = mgr
.list_versions(1)
.expect("test: operation should succeed");
assert_eq!(versions.len(), 5);
}
#[test]
fn test_storage_calculation() {
let mut history = VersionHistory::new(1, VersioningPolicy::default());
history.add_version(FileVersion::new(1, 1, 0, 1000, 100, [0u8; 32], 999));
history.add_version(FileVersion::new(1, 2, 100, 2000, 101, [0u8; 32], 999));
history.add_version(FileVersion::new(1, 3, 200, 3000, 102, [0u8; 32], 999));
assert_eq!(history.total_storage(), 6000);
}
}