1use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use modde_core::{NexusFileId, NexusModId};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DownloadMeta {
16 pub url: String,
17 #[serde(default)]
18 pub expected_hash: Option<u64>,
19 #[serde(default)]
20 pub bytes_downloaded: u64,
21 #[serde(default)]
22 pub total_bytes: Option<u64>,
23 #[serde(default)]
24 pub nexus_mod_id: Option<NexusModId>,
25 #[serde(default)]
26 pub nexus_file_id: Option<NexusFileId>,
27 #[serde(default)]
28 pub game_domain: Option<String>,
29 #[serde(default)]
30 pub mod_name: Option<String>,
31 #[serde(default)]
32 pub version: Option<String>,
33 #[serde(default = "default_status")]
34 pub status: String,
35}
36
37fn default_status() -> String {
38 "queued".to_string()
39}
40
41impl DownloadMeta {
42 pub fn load(path: &Path) -> Result<Self> {
44 let data =
45 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
46 serde_json::from_str(&data).with_context(|| format!("parsing {}", path.display()))
47 }
48
49 pub fn save(&self, path: &Path) -> Result<()> {
51 let data = serde_json::to_string_pretty(self)?;
52 if let Some(parent) = path.parent() {
53 std::fs::create_dir_all(parent)?;
54 }
55 std::fs::write(path, data).with_context(|| format!("writing {}", path.display()))
56 }
57}
58
59#[must_use]
63pub fn meta_path(download_path: &Path) -> PathBuf {
64 let mut p = download_path.as_os_str().to_owned();
65 p.push(".meta");
66 PathBuf::from(p)
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn test_meta_roundtrip() {
75 let dir = tempfile::tempdir().unwrap();
76 let mp = dir.path().join("test.zip.meta");
77
78 let meta = DownloadMeta {
79 url: "https://example.com/mod.zip".into(),
80 expected_hash: Some(0xDEAD_BEEF),
81 bytes_downloaded: 1024,
82 total_bytes: Some(4096),
83 nexus_mod_id: Some(NexusModId::from(42)),
84 nexus_file_id: Some(NexusFileId::from(99)),
85 game_domain: Some("skyrimspecialedition".into()),
86 mod_name: Some("Cool Mod".into()),
87 version: Some("1.2.3".into()),
88 status: "downloading".into(),
89 };
90
91 meta.save(&mp).unwrap();
92 let loaded = DownloadMeta::load(&mp).unwrap();
93
94 assert_eq!(loaded.url, meta.url);
95 assert_eq!(loaded.expected_hash, meta.expected_hash);
96 assert_eq!(loaded.bytes_downloaded, meta.bytes_downloaded);
97 assert_eq!(loaded.total_bytes, meta.total_bytes);
98 assert_eq!(loaded.nexus_mod_id, meta.nexus_mod_id);
99 assert_eq!(loaded.nexus_file_id, meta.nexus_file_id);
100 assert_eq!(loaded.game_domain, meta.game_domain);
101 assert_eq!(loaded.mod_name, meta.mod_name);
102 assert_eq!(loaded.version, meta.version);
103 assert_eq!(loaded.status, meta.status);
104 }
105
106 #[test]
107 fn test_meta_path() {
108 let p = meta_path(Path::new("/downloads/mod.zip"));
109 assert_eq!(p, PathBuf::from("/downloads/mod.zip.meta"));
110 }
111
112 #[test]
113 fn test_meta_defaults() {
114 let json = r#"{"url": "https://example.com/f.zip"}"#;
115 let meta: DownloadMeta = serde_json::from_str(json).unwrap();
116 assert_eq!(meta.bytes_downloaded, 0);
117 assert_eq!(meta.total_bytes, None);
118 assert_eq!(meta.expected_hash, None);
119 assert_eq!(meta.status, "queued");
120 }
121}