use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{MegaError, Result};
use sha2::{Digest, Sha256};
use std::io::Read;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadState {
pub upload_url: String,
pub file_key: [u8; 16],
pub nonce: [u8; 8],
pub chunk_macs: Vec<[u8; 16]>,
pub offset: u64,
pub file_size: u64,
pub file_name: String,
pub parent_handle: String,
pub file_hash: String,
pub created_at: i64,
}
impl UploadState {
pub fn new(
upload_url: String,
file_key: [u8; 16],
nonce: [u8; 8],
file_size: u64,
file_name: String,
parent_handle: String,
file_hash: String,
) -> Self {
Self {
upload_url,
file_key,
nonce,
chunk_macs: Vec::new(),
offset: 0,
file_size,
file_name,
parent_handle,
file_hash,
created_at: current_unix_timestamp(),
}
}
pub fn state_file_path<P: AsRef<Path>>(source_path: P) -> std::path::PathBuf {
let source = source_path.as_ref();
let file_name = source
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "upload".to_string());
source.with_file_name(format!("{}.megalib_upload", file_name))
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| MegaError::Custom(format!("Failed to serialize upload state: {}", e)))?;
std::fs::write(path, json)
.map_err(|e| MegaError::Custom(format!("Failed to write upload state: {}", e)))?;
Ok(())
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Option<Self>> {
let path = path.as_ref();
if !path.exists() {
return Ok(None);
}
let json = std::fs::read_to_string(path)
.map_err(|e| MegaError::Custom(format!("Failed to read upload state: {}", e)))?;
let state: Self = serde_json::from_str(&json)
.map_err(|e| MegaError::Custom(format!("Failed to parse upload state: {}", e)))?;
Ok(Some(state))
}
pub fn delete<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if path.exists() {
std::fs::remove_file(path)
.map_err(|e| MegaError::Custom(format!("Failed to delete upload state: {}", e)))?;
}
Ok(())
}
pub fn is_likely_valid(&self) -> bool {
let now = current_unix_timestamp();
let age_hours = (now - self.created_at) / 3600;
age_hours < 24
}
pub fn add_chunk_mac(&mut self, mac: [u8; 16], chunk_size: u64) {
self.chunk_macs.push(mac);
self.offset += chunk_size;
}
pub(crate) fn reset_for_new_upload_url(&mut self, upload_url: String) {
self.upload_url = upload_url;
self.chunk_macs.clear();
self.offset = 0;
self.created_at = current_unix_timestamp();
}
}
fn current_unix_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
pub fn calculate_file_hash<P: AsRef<Path>>(path: P) -> Result<String> {
let mut file = std::fs::File::open(path.as_ref())
.map_err(|e| MegaError::Custom(format!("Failed to open file for hashing: {}", e)))?;
let mut buffer = vec![0u8; 1024 * 1024]; let bytes_read = file
.read(&mut buffer)
.map_err(|e| MegaError::Custom(format!("Failed to read file for hashing: {}", e)))?;
let mut hasher = Sha256::new();
hasher.update(&buffer[..bytes_read]);
let hash = hasher.finalize();
Ok(hex::encode(hash))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_file_path() {
let path = UploadState::state_file_path("/home/user/myfile.zip");
assert!(
path.to_string_lossy()
.ends_with("myfile.zip.megalib_upload")
);
}
#[test]
fn test_upload_state_serialization() {
let state = UploadState::new(
"https://example.com/upload".to_string(),
[1u8; 16],
[2u8; 8],
1024,
"test.txt".to_string(),
"HANDLE123".to_string(),
"abc123".to_string(),
);
let json = serde_json::to_string(&state).unwrap();
let restored: UploadState = serde_json::from_str(&json).unwrap();
assert_eq!(state.upload_url, restored.upload_url);
assert_eq!(state.file_key, restored.file_key);
assert_eq!(state.nonce, restored.nonce);
assert_eq!(state.file_size, restored.file_size);
}
#[test]
fn test_add_chunk_mac() {
let mut state = UploadState::new(
"url".to_string(),
[0u8; 16],
[0u8; 8],
1000,
"file".to_string(),
"handle".to_string(),
"hash".to_string(),
);
assert_eq!(state.offset, 0);
assert!(state.chunk_macs.is_empty());
state.add_chunk_mac([1u8; 16], 100);
assert_eq!(state.offset, 100);
assert_eq!(state.chunk_macs.len(), 1);
assert_eq!(state.chunk_macs[0], [1u8; 16]);
state.add_chunk_mac([2u8; 16], 200);
assert_eq!(state.offset, 300);
assert_eq!(state.chunk_macs.len(), 2);
assert_eq!(state.chunk_macs[1], [2u8; 16]);
}
}