hyveos_core/
file_transfer.rs

1use std::{
2    fmt,
3    path::{Path, PathBuf},
4    str::FromStr,
5};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use ulid::Ulid;
10
11use crate::{
12    error::{Error, Result},
13    grpc,
14};
15
16#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct Cid {
19    pub id: Ulid,
20    pub hash: [u8; 32],
21}
22
23impl From<Cid> for grpc::Cid {
24    fn from(cid: Cid) -> Self {
25        Self {
26            hash: cid.hash.into(),
27            id: cid.id.into(),
28        }
29    }
30}
31
32impl TryFrom<grpc::Cid> for Cid {
33    type Error = Error;
34
35    fn try_from(cid: grpc::Cid) -> Result<Self> {
36        Ok(Self {
37            hash: cid.hash.try_into().map_err(|_| Error::InvalidFileHash)?,
38            id: cid.id.try_into()?,
39        })
40    }
41}
42
43impl fmt::Display for Cid {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        let ulid_str = self.id.to_string();
46        let hash_hex = hex::encode(self.hash);
47        write!(f, "{ulid_str}-{hash_hex}")
48    }
49}
50
51impl FromStr for Cid {
52    type Err = Error;
53
54    fn from_str(s: &str) -> Result<Self> {
55        let mut parts = s.splitn(2, '-');
56        let ulid_part = parts.next().ok_or(Error::InvalidCidFormat)?;
57        let hash_part = parts.next().ok_or(Error::InvalidCidFormat)?;
58
59        let id = Ulid::from_string(ulid_part).map_err(|_| Error::InvalidCidFormat)?;
60
61        let hash_bytes = hex::decode(hash_part).map_err(|_| Error::InvalidCidFormat)?;
62        if hash_bytes.len() != 32 {
63            return Err(Error::InvalidCidFormat);
64        }
65
66        let mut hash_array = [0u8; 32];
67        hash_array.copy_from_slice(&hash_bytes);
68
69        Ok(Cid {
70            id,
71            hash: hash_array,
72        })
73    }
74}
75
76impl TryFrom<&Path> for grpc::FilePath {
77    type Error = Error;
78
79    fn try_from(path: &Path) -> Result<Self> {
80        Ok(Self {
81            path: path
82                .to_str()
83                .map(ToString::to_string)
84                .ok_or(Error::InvalidFilePath)?,
85        })
86    }
87}
88
89impl TryFrom<PathBuf> for grpc::FilePath {
90    type Error = Error;
91
92    fn try_from(path: PathBuf) -> Result<Self> {
93        Ok(Self {
94            path: path
95                .to_str()
96                .map(ToString::to_string)
97                .ok_or(Error::InvalidFilePath)?,
98        })
99    }
100}
101
102impl From<grpc::FilePath> for PathBuf {
103    fn from(path: grpc::FilePath) -> Self {
104        path.path.into()
105    }
106}
107
108#[derive(Debug, Clone, Hash, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110pub enum DownloadEvent {
111    Progress(u64),
112    Ready(PathBuf),
113}
114
115impl TryFrom<DownloadEvent> for grpc::DownloadEvent {
116    type Error = Error;
117
118    fn try_from(event: DownloadEvent) -> Result<Self> {
119        let event = match event {
120            DownloadEvent::Progress(progress) => grpc::download_event::Event::Progress(progress),
121            DownloadEvent::Ready(path) => grpc::download_event::Event::Ready(path.try_into()?),
122        };
123
124        Ok(Self { event: Some(event) })
125    }
126}
127
128impl TryFrom<grpc::DownloadEvent> for DownloadEvent {
129    type Error = Error;
130
131    fn try_from(event: grpc::DownloadEvent) -> Result<Self> {
132        Ok(match event.event.ok_or(Error::MissingEvent)? {
133            grpc::download_event::Event::Progress(progress) => DownloadEvent::Progress(progress),
134            grpc::download_event::Event::Ready(path) => DownloadEvent::Ready(path.into()),
135        })
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use std::str::FromStr;
142
143    use super::*;
144
145    #[test]
146    fn test_cid_to_string() {
147        let cid = Cid {
148            id: Ulid::from_string("01GZMC49M8599PQPNGDSAX6X1F").unwrap(),
149            hash: [
150                0xFF, 0xD2, 0x34, 0x1A, 0x00, 0xAB, 0xCD, 0xEF, 0x00, 0x11, 0x33, 0x55, 0x77, 0x99,
151                0xBB, 0xDD, 0xCC, 0xEE, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
152                0xFF, 0x12, 0x34, 0x56,
153            ],
154        };
155
156        let s = cid.to_string();
157
158        let expected_str = "01GZMC49M8599PQPNGDSAX6X1F-ffd2341a00abcdef001133557799bbddccee5566778899aabbccddeeff123456";
159
160        assert_eq!(s, expected_str, "Cid::to_string failed");
161    }
162
163    #[test]
164    fn test_cid_from_str() {
165        let input_str = "01GZMC49M8599PQPNGDSAX6X1F-ffd2341a00abcdef001133557799bbddccee5566778899aabbccddeeff123456";
166
167        let cid = Cid::from_str(input_str).expect("Parsing valid CID");
168
169        assert_eq!(
170            cid.id,
171            Ulid::from_string("01GZMC49M8599PQPNGDSAX6X1F").unwrap(),
172            "The ULID part was incorrectly parsed"
173        );
174
175        let expected_hash = [
176            0xFF, 0xD2, 0x34, 0x1A, 0x00, 0xAB, 0xCD, 0xEF, 0x00, 0x11, 0x33, 0x55, 0x77, 0x99,
177            0xBB, 0xDD, 0xCC, 0xEE, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
178            0xFF, 0x12, 0x34, 0x56,
179        ];
180        assert_eq!(
181            cid.hash, expected_hash,
182            "The hash part was incorrectly parsed"
183        );
184    }
185}