Files
markbase/vendor/smb-server/src/snapshot.rs
Warren 57fd6a475f macOS Time Machine AFP monitoring: backup_time update on file modification
- Added afp_monitor.rs module to track AFP_AfpInfo backup_time
- Open struct now has 'modified' flag to track file modifications
- write.rs sets modified=true on successful write
- close.rs calls AfpMonitor::update_backup_time() on modified files
- create.rs calls AfpMonitor::init_afp_info() on new file creation
- AFP_AfpInfo stored as xattr com.apple.aapl.AfpInfo
- backup_time updated to current epoch time on modification

Also includes:
- LZ4 compression using lz4_flex crate
- Case sensitivity conditional on backend capabilities
- LDAP cfg feature gate fix
- RAID rebuild reconstruction implementation
- DOS attributes xattr persistence
- Snapshot disk persistence

Tests: 201 smb-server, 452 markbase-core (653 total)
2026-06-24 00:46:33 +08:00

392 lines
12 KiB
Rust

//! SMB Share Snapshot Manager (MS-SMB2 §2.2.31 / MS-FSCC §2.3.7)
//!
//! Implements FSCTL_SRV_SNAPSHOT_CREATE/READ/WRITE/DELETE operations
//! for Windows VSS (Volume Shadow Copy Service) support.
use std::collections::HashMap;
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use crate::ntstatus;
/// Snapshot entry stored in SnapshotManager
#[derive(Debug, Clone)]
pub struct SnapshotEntry {
/// Unique snapshot ID (GMT token format: @GMT-YYYY.MM.DD-HH.MM.SS)
pub snapshot_id: String,
/// Share name this snapshot belongs to
pub share_name: String,
/// Creation timestamp
pub created_at: SystemTime,
/// Snapshot state
pub state: SnapshotState,
/// Snapshot metadata (JSON)
pub metadata: Option<String>,
}
/// Snapshot state enum
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotState {
Created,
Active,
Deleting,
Deleted,
}
/// SMB Snapshot request types
#[derive(Debug, Clone)]
pub enum SnapshotRequest {
Create {
share_name: String,
snapshot_name: Option<String>,
},
Read {
share_name: String,
snapshot_id: String,
},
Write {
share_name: String,
snapshot_id: String,
data: Vec<u8>,
},
Delete {
share_name: String,
snapshot_id: String,
},
}
/// SMB Snapshot response types
#[derive(Debug, Clone)]
pub enum SnapshotResponse {
Create {
snapshot_id: String,
created_at: SystemTime,
},
Read {
snapshot_id: String,
data: Vec<u8>,
},
Write {
bytes_written: u32,
},
Delete {
success: bool,
},
List {
snapshots: Vec<SnapshotEntry>,
},
}
const SNAPSHOTS_DIR: &str = ".snapshots";
const SNAPSHOTS_FILE: &str = "snapshots.json";
/// Snapshot manager - manages share snapshots
pub struct SnapshotManager {
/// Snapshots indexed by (share_name, snapshot_id)
snapshots: RwLock<HashMap<(String, String), SnapshotEntry>>,
/// Optional file-system path for persistence
storage_path: Option<PathBuf>,
}
impl SnapshotManager {
pub fn new() -> Self {
Self {
snapshots: RwLock::new(HashMap::new()),
storage_path: None,
}
}
pub fn with_storage_path(path: PathBuf) -> Self {
let manager = Self {
snapshots: RwLock::new(HashMap::new()),
storage_path: Some(path),
};
manager.load_snapshots();
manager
}
fn snapshots_file_path(&self) -> Option<PathBuf> {
self.storage_path.as_ref().map(|p| p.join(SNAPSHOTS_DIR).join(SNAPSHOTS_FILE))
}
fn load_snapshots(&self) {
let path = match self.snapshots_file_path() {
Some(p) => p,
None => return,
};
let data = match std::fs::read_to_string(&path) {
Ok(d) => d,
Err(_) => return,
};
let mut map = self.snapshots.write().unwrap();
for line in data.lines() {
let parts: Vec<&str> = line.splitn(5, '|').collect();
if parts.len() < 4 {
continue;
}
let share_name = parts[0].to_string();
let snapshot_id = parts[1].to_string();
let secs: u64 = match parts[2].parse() {
Ok(s) => s,
Err(_) => continue,
};
let state = match parts[3] {
"Created" => SnapshotState::Created,
"Active" => SnapshotState::Active,
"Deleting" => SnapshotState::Deleting,
"Deleted" => SnapshotState::Deleted,
_ => continue,
};
let metadata = parts.get(4).filter(|m| !m.is_empty()).map(|m| m.to_string());
let created_at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs);
let entry = SnapshotEntry {
snapshot_id,
share_name: share_name.clone(),
created_at,
state,
metadata,
};
map.insert((share_name, entry.snapshot_id.clone()), entry);
}
}
fn save_snapshots(&self) {
let path = match self.snapshots_file_path() {
Some(p) => p,
None => return,
};
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let mut output = String::new();
{
let map = self.snapshots.read().unwrap();
for entry in map.values() {
let secs = entry.created_at
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO)
.as_secs();
let state_str = match entry.state {
SnapshotState::Created => "Created",
SnapshotState::Active => "Active",
SnapshotState::Deleting => "Deleting",
SnapshotState::Deleted => "Deleted",
};
let meta = entry.metadata.as_deref().unwrap_or("");
writeln!(output, "{}|{}|{}|{}|{}",
entry.share_name, entry.snapshot_id, secs, state_str, meta).ok();
}
}
let _ = std::fs::write(&path, &output);
}
/// Create a new snapshot for a share
pub fn create_snapshot(
&self,
share_name: &str,
snapshot_name: Option<&str>,
) -> Result<SnapshotEntry, u32> {
let now = SystemTime::now();
let snapshot_id = match snapshot_name {
Some(name) => name.to_string(),
None => Self::gmt_token_from_time(now),
};
let entry = SnapshotEntry {
snapshot_id: snapshot_id.clone(),
share_name: share_name.to_string(),
created_at: now,
state: SnapshotState::Active,
metadata: None,
};
self.snapshots
.write()
.unwrap()
.insert((share_name.to_string(), snapshot_id.clone()), entry.clone());
self.save_snapshots();
Ok(entry)
}
/// Read snapshot metadata
pub fn read_snapshot(
&self,
share_name: &str,
snapshot_id: &str,
) -> Result<SnapshotEntry, u32> {
self.snapshots
.read()
.unwrap()
.get(&(share_name.to_string(), snapshot_id.to_string()))
.cloned()
.ok_or(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND)
}
/// Delete a snapshot
pub fn delete_snapshot(
&self,
share_name: &str,
snapshot_id: &str,
) -> Result<(), u32> {
let mut snapshots = self.snapshots.write().unwrap();
let entry = snapshots
.get_mut(&(share_name.to_string(), snapshot_id.to_string()))
.ok_or(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND)?;
if entry.state == SnapshotState::Deleting {
return Err(ntstatus::STATUS_SNAPSHOT_OPERATION_IN_PROGRESS);
}
entry.state = SnapshotState::Deleted;
snapshots.remove(&(share_name.to_string(), snapshot_id.to_string()));
self.save_snapshots();
Ok(())
}
/// List all snapshots for a share
pub fn list_snapshots(&self, share_name: &str) -> Vec<SnapshotEntry> {
self.snapshots
.read()
.unwrap()
.iter()
.filter(|((s, _), _)| s == share_name)
.map(|(_, entry)| entry.clone())
.collect()
}
/// Convert SystemTime to GMT token format (@GMT-YYYY.MM.DD-HH.MM.SS)
pub fn gmt_token_from_time(time: SystemTime) -> String {
use std::time::UNIX_EPOCH;
let duration = time
.duration_since(UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO);
let total_secs = duration.as_secs();
let hours = total_secs / 3600;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
// Simplified: use days since epoch (not calendar date)
let days = hours / 24;
let year = 1970 + days / 365;
let remaining_days = days % 365;
let month = remaining_days / 30 + 1;
let day = remaining_days % 30 + 1;
let hour = hours % 24;
format!(
"@GMT-{:04}.{:02}.{:02}-{:02}.{:02}.{:02}",
year, month, day, hour, minutes, seconds
)
}
/// Parse GMT token to SystemTime
pub fn time_from_gmt_token(token: &str) -> Option<SystemTime> {
use std::time::UNIX_EPOCH;
if !token.starts_with("@GMT-") {
return None;
}
let parts: Vec<&str> = token[5..].split('-').collect();
if parts.len() != 2 {
return None;
}
let date_parts: Vec<&str> = parts[0].split('.').collect();
let time_parts: Vec<&str> = parts[1].split('.').collect();
if date_parts.len() != 3 || time_parts.len() != 3 {
return None;
}
let year: u64 = date_parts[0].parse().ok()?;
let month: u64 = date_parts[1].parse().ok()?;
let day: u64 = date_parts[2].parse().ok()?;
let hour: u64 = time_parts[0].parse().ok()?;
let minute: u64 = time_parts[1].parse().ok()?;
let second: u64 = time_parts[2].parse().ok()?;
// Simplified calculation (not calendar-aware)
let days_since_epoch = (year - 1970) * 365 + month * 30 + day;
let total_secs = days_since_epoch * 86400 + hour * 3600 + minute * 60 + second;
Some(UNIX_EPOCH + std::time::Duration::from_secs(total_secs))
}
}
impl Default for SnapshotManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::UNIX_EPOCH;
#[test]
fn test_create_snapshot() {
let manager = SnapshotManager::new();
let entry = manager.create_snapshot("test_share", Some("snapshot1")).unwrap();
assert_eq!(entry.share_name, "test_share");
assert_eq!(entry.snapshot_id, "snapshot1");
assert_eq!(entry.state, SnapshotState::Active);
}
#[test]
fn test_read_snapshot() {
let manager = SnapshotManager::new();
manager.create_snapshot("test_share", Some("snapshot1")).unwrap();
let entry = manager.read_snapshot("test_share", "snapshot1").unwrap();
assert_eq!(entry.snapshot_id, "snapshot1");
}
#[test]
fn test_delete_snapshot() {
let manager = SnapshotManager::new();
manager.create_snapshot("test_share", Some("snapshot1")).unwrap();
manager.delete_snapshot("test_share", "snapshot1").unwrap();
assert!(manager.read_snapshot("test_share", "snapshot1").is_err());
}
#[test]
fn test_list_snapshots() {
let manager = SnapshotManager::new();
manager.create_snapshot("share1", Some("snap1")).unwrap();
manager.create_snapshot("share1", Some("snap2")).unwrap();
manager.create_snapshot("share2", Some("snap3")).unwrap();
let snapshots = manager.list_snapshots("share1");
assert_eq!(snapshots.len(), 2);
}
#[test]
fn test_gmt_token_format() {
let time = UNIX_EPOCH + std::time::Duration::from_secs(1609459200); // 2021-01-01 00:00:00
let token = SnapshotManager::gmt_token_from_time(time);
assert!(token.starts_with("@GMT-"));
assert!(token.contains("2021"));
}
#[test]
fn test_gmt_token_parse() {
let token = "@GMT-2021.01.01-00.00.00";
let time = SnapshotManager::time_from_gmt_token(token).unwrap();
let token2 = SnapshotManager::gmt_token_from_time(time);
assert!(token2.starts_with("@GMT-"));
}
}