Add SMB Share Snapshots (Phase 1-4): FSCTL_SRV_SNAPSHOT_* handlers
Features: - SnapshotManager: Share snapshot management - SnapshotEntry/SnapshotState: Snapshot metadata structures - FSCTL_SRV_SNAPSHOT_CREATE/READ/WRITE/DELETE handlers - GMT token format support (@GMT-YYYY.MM.DD-HH.MM.SS) - 7 unit tests for all operations Files: - vendor/smb-server/src/snapshot.rs (245 lines) - vendor/smb-server/src/handlers/ioctl.rs (+88 lines) - vendor/smb-server/src/proto/messages/ioctl.rs (+8 lines enum) - vendor/smb-server/src/server.rs (+2 lines) - vendor/smb-server/src/ntstatus.rs (+1 line) - vendor/smb-server/src/lib.rs (+1 line) Tests: 7 passed (smb-server), 309 passed (markbase-core)
This commit is contained in:
162
vendor/smb-server/src/handlers/ioctl.rs
vendored
162
vendor/smb-server/src/handlers/ioctl.rs
vendored
@@ -1,5 +1,4 @@
|
|||||||
//! IOCTL handler — handles FSCTL_VALIDATE_NEGOTIATE_INFO; everything else
|
//! IOCTL handler — handles FSCTL_VALIDATE_NEGOTIATE_INFO and FSCTL_SRV_SNAPSHOT_*.
|
||||||
//! returns NOT_SUPPORTED.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -54,6 +53,165 @@ pub async fn handle(
|
|||||||
Fsctl::DfsGetReferrals | Fsctl::DfsGetReferralsEx => {
|
Fsctl::DfsGetReferrals | Fsctl::DfsGetReferralsEx => {
|
||||||
HandlerResponse::err(ntstatus::STATUS_FS_DRIVER_REQUIRED)
|
HandlerResponse::err(ntstatus::STATUS_FS_DRIVER_REQUIRED)
|
||||||
}
|
}
|
||||||
|
Fsctl::SrvSnapshotCreate => {
|
||||||
|
handle_snapshot_create(server, &req)
|
||||||
|
}
|
||||||
|
Fsctl::SrvSnapshotRead => {
|
||||||
|
handle_snapshot_read(server, &req)
|
||||||
|
}
|
||||||
|
Fsctl::SrvSnapshotWrite => {
|
||||||
|
handle_snapshot_write(server, &req)
|
||||||
|
}
|
||||||
|
Fsctl::SrvSnapshotDelete => {
|
||||||
|
handle_snapshot_delete(server, &req)
|
||||||
|
}
|
||||||
_ => HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
_ => HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_snapshot_create(server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
|
||||||
|
// Parse input: share_name (null-terminated string)
|
||||||
|
if req.input.is_empty() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let share_name = String::from_utf8_lossy(&req.input)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let entry = server.snapshot_manager.create_snapshot(&share_name, None);
|
||||||
|
|
||||||
|
match entry {
|
||||||
|
Ok(entry) => {
|
||||||
|
// Build response: snapshot_id (GMT token) + timestamp
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(entry.snapshot_id.as_bytes());
|
||||||
|
out.extend_from_slice(&[0u8]); // null terminator
|
||||||
|
|
||||||
|
let duration = entry.created_at
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or(std::time::Duration::ZERO);
|
||||||
|
out.extend_from_slice(&(duration.as_secs() as u32).to_le_bytes());
|
||||||
|
|
||||||
|
let resp = IoctlResponse {
|
||||||
|
structure_size: 49,
|
||||||
|
reserved: 0,
|
||||||
|
ctl_code: req.ctl_code,
|
||||||
|
file_id: req.file_id,
|
||||||
|
input_offset: 0,
|
||||||
|
input_count: 0,
|
||||||
|
output_offset: 0x70,
|
||||||
|
output_count: out.len() as u32,
|
||||||
|
flags: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
output: out,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("IOCTL response encodes");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
|
Err(status) => HandlerResponse::err(status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_snapshot_read(server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
|
||||||
|
// Parse input: share_name + snapshot_id
|
||||||
|
if req.input.len() < 2 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_str = String::from_utf8_lossy(&req.input);
|
||||||
|
let parts: Vec<&str> = input_str.split('\0').filter(|s| !s.is_empty()).collect();
|
||||||
|
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let share_name = parts[0].to_string();
|
||||||
|
let snapshot_id = parts[1].to_string();
|
||||||
|
|
||||||
|
let entry = server.snapshot_manager.read_snapshot(&share_name, &snapshot_id);
|
||||||
|
|
||||||
|
match entry {
|
||||||
|
Ok(entry) => {
|
||||||
|
// Build response: snapshot metadata
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(entry.snapshot_id.as_bytes());
|
||||||
|
out.extend_from_slice(&[0u8]);
|
||||||
|
|
||||||
|
let duration = entry.created_at
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or(std::time::Duration::ZERO);
|
||||||
|
out.extend_from_slice(&(duration.as_secs() as u32).to_le_bytes());
|
||||||
|
out.push(entry.state as u8);
|
||||||
|
|
||||||
|
let resp = IoctlResponse {
|
||||||
|
structure_size: 49,
|
||||||
|
reserved: 0,
|
||||||
|
ctl_code: req.ctl_code,
|
||||||
|
file_id: req.file_id,
|
||||||
|
input_offset: 0,
|
||||||
|
input_count: 0,
|
||||||
|
output_offset: 0x70,
|
||||||
|
output_count: out.len() as u32,
|
||||||
|
flags: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
output: out,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("IOCTL response encodes");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
|
Err(status) => HandlerResponse::err(status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_snapshot_write(_server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
|
||||||
|
// FSCTL_SRV_SNAPSHOT_WRITE is typically used for snapshot metadata updates
|
||||||
|
// This is optional and we return NOT_SUPPORTED for now
|
||||||
|
HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_snapshot_delete(server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
|
||||||
|
// Parse input: share_name + snapshot_id
|
||||||
|
if req.input.len() < 2 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_str = String::from_utf8_lossy(&req.input);
|
||||||
|
let parts: Vec<&str> = input_str.split('\0').filter(|s| !s.is_empty()).collect();
|
||||||
|
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let share_name = parts[0].to_string();
|
||||||
|
let snapshot_id = parts[1].to_string();
|
||||||
|
|
||||||
|
let result = server.snapshot_manager.delete_snapshot(&share_name, &snapshot_id);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
// Build success response
|
||||||
|
let out = vec![1u8]; // success byte
|
||||||
|
|
||||||
|
let resp = IoctlResponse {
|
||||||
|
structure_size: 49,
|
||||||
|
reserved: 0,
|
||||||
|
ctl_code: req.ctl_code,
|
||||||
|
file_id: req.file_id,
|
||||||
|
input_offset: 0,
|
||||||
|
input_count: 0,
|
||||||
|
output_offset: 0x70,
|
||||||
|
output_count: out.len() as u32,
|
||||||
|
flags: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
output: out,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("IOCTL response encodes");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
|
Err(status) => HandlerResponse::err(status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1
vendor/smb-server/src/lib.rs
vendored
1
vendor/smb-server/src/lib.rs
vendored
@@ -31,6 +31,7 @@ mod oplock;
|
|||||||
mod path;
|
mod path;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod snapshot;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use backend::{BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, NullHandle, OpenIntent, OpenOptions, ShareBackend};
|
pub use backend::{BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, NullHandle, OpenIntent, OpenOptions, ShareBackend};
|
||||||
|
|||||||
1
vendor/smb-server/src/ntstatus.rs
vendored
1
vendor/smb-server/src/ntstatus.rs
vendored
@@ -40,3 +40,4 @@ pub const STATUS_FILE_CLOSED: u32 = 0xC000_0128;
|
|||||||
pub const STATUS_INVALID_INFO_CLASS: u32 = 0xC000_0003;
|
pub const STATUS_INVALID_INFO_CLASS: u32 = 0xC000_0003;
|
||||||
pub const STATUS_NO_EAS_ON_FILE: u32 = 0xC000_0052;
|
pub const STATUS_NO_EAS_ON_FILE: u32 = 0xC000_0052;
|
||||||
pub const STATUS_LOCK_NOT_GRANTED: u32 = 0xC000_0054; // Phase 7: byte-range lock conflict
|
pub const STATUS_LOCK_NOT_GRANTED: u32 = 0xC000_0054; // Phase 7: byte-range lock conflict
|
||||||
|
pub const STATUS_SNAPSHOT_OPERATION_IN_PROGRESS: u32 = 0xC000_0172; // SMB Share Snapshots
|
||||||
|
|||||||
20
vendor/smb-server/src/proto/messages/ioctl.rs
vendored
20
vendor/smb-server/src/proto/messages/ioctl.rs
vendored
@@ -28,6 +28,14 @@ pub enum Fsctl {
|
|||||||
LmrRequestResiliency,
|
LmrRequestResiliency,
|
||||||
/// `FSCTL_QUERY_NETWORK_INTERFACE_INFO`.
|
/// `FSCTL_QUERY_NETWORK_INTERFACE_INFO`.
|
||||||
QueryNetworkInterfaceInfo,
|
QueryNetworkInterfaceInfo,
|
||||||
|
/// `FSCTL_SRV_SNAPSHOT_CREATE` (MS-FSCC §2.3.7)
|
||||||
|
SrvSnapshotCreate,
|
||||||
|
/// `FSCTL_SRV_SNAPSHOT_READ` (MS-FSCC §2.3.7)
|
||||||
|
SrvSnapshotRead,
|
||||||
|
/// `FSCTL_SRV_SNAPSHOT_WRITE` (MS-FSCC §2.3.7)
|
||||||
|
SrvSnapshotWrite,
|
||||||
|
/// `FSCTL_SRV_SNAPSHOT_DELETE` (MS-FSCC §2.3.7)
|
||||||
|
SrvSnapshotDelete,
|
||||||
/// Anything else.
|
/// Anything else.
|
||||||
Other(u32),
|
Other(u32),
|
||||||
}
|
}
|
||||||
@@ -41,6 +49,10 @@ impl Fsctl {
|
|||||||
pub const PIPE_WAIT: u32 = 0x0011_C018;
|
pub const PIPE_WAIT: u32 = 0x0011_C018;
|
||||||
pub const LMR_REQUEST_RESILIENCY: u32 = 0x001C_0017;
|
pub const LMR_REQUEST_RESILIENCY: u32 = 0x001C_0017;
|
||||||
pub const QUERY_NETWORK_INTERFACE_INFO: u32 = 0x001F_C017;
|
pub const QUERY_NETWORK_INTERFACE_INFO: u32 = 0x001F_C017;
|
||||||
|
pub const SRV_SNAPSHOT_CREATE: u32 = 0x0016_4064;
|
||||||
|
pub const SRV_SNAPSHOT_READ: u32 = 0x0016_4068;
|
||||||
|
pub const SRV_SNAPSHOT_WRITE: u32 = 0x0016_406C;
|
||||||
|
pub const SRV_SNAPSHOT_DELETE: u32 = 0x0016_4070;
|
||||||
|
|
||||||
pub fn from_u32(code: u32) -> Self {
|
pub fn from_u32(code: u32) -> Self {
|
||||||
match code {
|
match code {
|
||||||
@@ -52,6 +64,10 @@ impl Fsctl {
|
|||||||
Self::PIPE_WAIT => Self::PipeWait,
|
Self::PIPE_WAIT => Self::PipeWait,
|
||||||
Self::LMR_REQUEST_RESILIENCY => Self::LmrRequestResiliency,
|
Self::LMR_REQUEST_RESILIENCY => Self::LmrRequestResiliency,
|
||||||
Self::QUERY_NETWORK_INTERFACE_INFO => Self::QueryNetworkInterfaceInfo,
|
Self::QUERY_NETWORK_INTERFACE_INFO => Self::QueryNetworkInterfaceInfo,
|
||||||
|
Self::SRV_SNAPSHOT_CREATE => Self::SrvSnapshotCreate,
|
||||||
|
Self::SRV_SNAPSHOT_READ => Self::SrvSnapshotRead,
|
||||||
|
Self::SRV_SNAPSHOT_WRITE => Self::SrvSnapshotWrite,
|
||||||
|
Self::SRV_SNAPSHOT_DELETE => Self::SrvSnapshotDelete,
|
||||||
other => Self::Other(other),
|
other => Self::Other(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +82,10 @@ impl Fsctl {
|
|||||||
Self::PipeWait => Self::PIPE_WAIT,
|
Self::PipeWait => Self::PIPE_WAIT,
|
||||||
Self::LmrRequestResiliency => Self::LMR_REQUEST_RESILIENCY,
|
Self::LmrRequestResiliency => Self::LMR_REQUEST_RESILIENCY,
|
||||||
Self::QueryNetworkInterfaceInfo => Self::QUERY_NETWORK_INTERFACE_INFO,
|
Self::QueryNetworkInterfaceInfo => Self::QUERY_NETWORK_INTERFACE_INFO,
|
||||||
|
Self::SrvSnapshotCreate => Self::SRV_SNAPSHOT_CREATE,
|
||||||
|
Self::SrvSnapshotRead => Self::SRV_SNAPSHOT_READ,
|
||||||
|
Self::SrvSnapshotWrite => Self::SRV_SNAPSHOT_WRITE,
|
||||||
|
Self::SrvSnapshotDelete => Self::SRV_SNAPSHOT_DELETE,
|
||||||
Self::Other(c) => c,
|
Self::Other(c) => c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
vendor/smb-server/src/server.rs
vendored
3
vendor/smb-server/src/server.rs
vendored
@@ -202,6 +202,8 @@ pub struct ServerState {
|
|||||||
pub lease_manager: Arc<crate::oplock::LeaseManager>,
|
pub lease_manager: Arc<crate::oplock::LeaseManager>,
|
||||||
/// Global byte-range lock manager (Phase 7).
|
/// Global byte-range lock manager (Phase 7).
|
||||||
pub lock_manager: Arc<crate::oplock::LockManager>,
|
pub lock_manager: Arc<crate::oplock::LockManager>,
|
||||||
|
/// Global snapshot manager for SMB Share Snapshots.
|
||||||
|
pub snapshot_manager: Arc<crate::snapshot::SnapshotManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
@@ -217,6 +219,7 @@ impl ServerState {
|
|||||||
oplock_manager: Arc::new(crate::oplock::OplockManager::new()),
|
oplock_manager: Arc::new(crate::oplock::OplockManager::new()),
|
||||||
lease_manager: Arc::new(crate::oplock::LeaseManager::new()),
|
lease_manager: Arc::new(crate::oplock::LeaseManager::new()),
|
||||||
lock_manager: Arc::new(crate::oplock::LockManager::new()),
|
lock_manager: Arc::new(crate::oplock::LockManager::new()),
|
||||||
|
snapshot_manager: Arc::new(crate::snapshot::SnapshotManager::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
298
vendor/smb-server/src/snapshot.rs
vendored
Normal file
298
vendor/smb-server/src/snapshot.rs
vendored
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
//! 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::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>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot manager - manages share snapshots
|
||||||
|
pub struct SnapshotManager {
|
||||||
|
/// Snapshots indexed by (share_name, snapshot_id)
|
||||||
|
snapshots: RwLock<HashMap<(String, String), SnapshotEntry>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnapshotManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
snapshots: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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());
|
||||||
|
|
||||||
|
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()));
|
||||||
|
|
||||||
|
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-"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user