use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum NodeType {
File = 0,
Folder = 1,
Root = 2,
Inbox = 3,
Trash = 4,
Contact = 8,
Network = 9,
}
impl NodeType {
pub fn from_i64(t: i64) -> Option<Self> {
match t {
0 => Some(NodeType::File),
1 => Some(NodeType::Folder),
2 => Some(NodeType::Root),
3 => Some(NodeType::Inbox),
4 => Some(NodeType::Trash),
8 => Some(NodeType::Contact),
9 => Some(NodeType::Network),
_ => None,
}
}
pub fn is_container(&self) -> bool {
matches!(
self,
NodeType::Folder
| NodeType::Root
| NodeType::Inbox
| NodeType::Trash
| NodeType::Network
)
}
}
#[derive(Debug, Clone)]
pub struct Node {
pub name: String,
pub handle: String,
pub parent_handle: Option<String>,
pub node_type: NodeType,
pub size: u64,
pub timestamp: i64,
pub(crate) key: Vec<u8>,
pub(crate) path: Option<String>,
pub(crate) link: Option<String>,
pub(crate) file_attr: Option<String>,
pub(crate) share_key: Option<[u8; 16]>,
pub(crate) share_handle: Option<String>,
pub(crate) is_inshare: bool,
pub(crate) is_outshare: bool,
pub(crate) share_access: Option<i32>,
}
impl Node {
pub fn is_file(&self) -> bool {
self.node_type == NodeType::File
}
pub fn is_folder(&self) -> bool {
self.node_type.is_container()
}
pub fn is_contact(&self) -> bool {
self.node_type == NodeType::Contact
}
pub fn path(&self) -> Option<&str> {
self.path.as_deref()
}
pub fn get_key(&self) -> Option<String> {
if self.key.is_empty() {
None
} else {
Some(crate::base64::base64url_encode(&self.key))
}
}
pub fn get_link(&self, include_key: bool) -> Option<String> {
let link_handle = self.link.as_ref()?;
if include_key {
let key = self.get_key()?;
Some(format!("https://mega.nz/file/{}#{}", link_handle, key))
} else {
Some(format!("https://mega.nz/file/{}", link_handle))
}
}
pub fn is_exported(&self) -> bool {
self.link.is_some()
}
pub fn is_writable(&self) -> bool {
if self.is_inshare {
self.share_access.unwrap_or(-1) >= 1
} else {
matches!(
self.node_type,
NodeType::File
| NodeType::Folder
| NodeType::Root
| NodeType::Trash
| NodeType::Inbox
)
}
}
pub fn share_key(&self) -> Option<&[u8; 16]> {
self.share_key.as_ref()
}
pub fn is_inshare(&self) -> bool {
self.is_inshare
}
pub fn is_outshare(&self) -> bool {
self.is_outshare
}
pub fn share_access(&self) -> Option<i32> {
self.share_access
}
}
#[derive(Debug, Clone, Copy)]
pub struct Quota {
pub total: u64,
pub used: u64,
}
impl Quota {
pub fn free(&self) -> u64 {
self.total.saturating_sub(self.used)
}
pub fn usage_percent(&self) -> f64 {
if self.total == 0 {
0.0
} else {
(self.used as f64 / self.total as f64) * 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_type_conversion() {
assert_eq!(NodeType::from_i64(0), Some(NodeType::File));
assert_eq!(NodeType::from_i64(1), Some(NodeType::Folder));
assert_eq!(NodeType::from_i64(2), Some(NodeType::Root));
assert_eq!(NodeType::from_i64(3), Some(NodeType::Inbox));
assert_eq!(NodeType::from_i64(4), Some(NodeType::Trash));
assert_eq!(NodeType::from_i64(8), Some(NodeType::Contact));
assert_eq!(NodeType::from_i64(9), Some(NodeType::Network));
assert_eq!(NodeType::from_i64(99), None);
}
#[test]
fn test_node_type_properties() {
assert!(!NodeType::File.is_container());
assert!(NodeType::Folder.is_container());
assert!(NodeType::Root.is_container());
assert!(NodeType::Inbox.is_container());
assert!(NodeType::Trash.is_container());
assert!(NodeType::Network.is_container());
assert!(!NodeType::Contact.is_container());
}
#[test]
fn test_node_helper_methods() {
let file_node = Node {
name: "test.txt".to_string(),
handle: "h1".to_string(),
parent_handle: None,
node_type: NodeType::File,
size: 100,
timestamp: 0,
key: vec![],
path: None,
link: None,
file_attr: None,
share_key: None,
share_handle: None,
is_inshare: false,
is_outshare: false,
share_access: None,
};
assert!(file_node.is_file());
assert!(!file_node.is_folder());
assert!(!file_node.is_contact());
let folder_node = Node {
name: "Folder".to_string(),
handle: "h2".to_string(),
parent_handle: None,
node_type: NodeType::Folder,
size: 0,
timestamp: 0,
key: vec![],
path: None,
link: None,
file_attr: None,
share_key: None,
share_handle: None,
is_inshare: false,
is_outshare: false,
share_access: None,
};
assert!(!folder_node.is_file());
assert!(folder_node.is_folder());
}
#[test]
fn test_node_link_generation() {
let node = Node {
name: "test.txt".to_string(),
handle: "h1".to_string(),
parent_handle: None,
node_type: NodeType::File,
size: 100,
timestamp: 0,
key: vec![1, 2, 3, 4], path: None,
link: Some("LINK_HANDLE".to_string()),
file_attr: None,
share_key: None,
share_handle: None,
is_inshare: false,
is_outshare: false,
share_access: None,
};
assert!(node.is_exported());
let link_no_key = node.get_link(false).unwrap();
assert_eq!(link_no_key, "https://mega.nz/file/LINK_HANDLE");
let link_with_key = node.get_link(true).unwrap();
assert!(link_with_key.contains("#"));
assert!(link_with_key.contains("LINK_HANDLE"));
}
#[test]
fn test_quota_calculations() {
let quota = Quota {
total: 1000,
used: 250,
};
assert_eq!(quota.free(), 750);
assert_eq!(quota.usage_percent(), 25.0);
let empty_quota = Quota { total: 0, used: 0 };
assert_eq!(empty_quota.usage_percent(), 0.0);
}
}