1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use soma_ops::Timestamp;
5
6use crate::{HostId, RequestError, request::validate_absolute_path};
7
8const MAX_TRANSFER_BYTES: u64 = 1024 * 1024 * 1024 * 1024;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct TransferRequest {
13 source_host: HostId,
14 source_path: PathBuf,
15 destination_host: HostId,
16 destination_path: PathBuf,
17 max_bytes: u64,
18 deadline: Timestamp,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 expected_source_sha256: Option<String>,
21}
22
23impl TransferRequest {
24 pub fn new(
26 source_host: HostId,
27 source_path: impl Into<PathBuf>,
28 destination_host: HostId,
29 destination_path: impl Into<PathBuf>,
30 max_bytes: u64,
31 deadline: Timestamp,
32 ) -> Result<Self, RequestError> {
33 if max_bytes == 0 || max_bytes > MAX_TRANSFER_BYTES {
34 return Err(RequestError::InvalidTransferLimit {
35 bytes: max_bytes,
36 max: MAX_TRANSFER_BYTES,
37 });
38 }
39 Ok(Self {
40 source_host,
41 source_path: validate_absolute_path(source_path.into())?,
42 destination_host,
43 destination_path: validate_absolute_path(destination_path.into())?,
44 max_bytes,
45 deadline,
46 expected_source_sha256: None,
47 })
48 }
49
50 #[must_use]
56 pub fn with_expected_source_sha256(mut self, sha256: impl Into<String>) -> Self {
57 self.expected_source_sha256 = Some(sha256.into());
58 self
59 }
60
61 #[must_use]
63 pub fn expected_source_sha256(&self) -> Option<&str> {
64 self.expected_source_sha256.as_deref()
65 }
66
67 pub fn validate_at(&self, now: Timestamp) -> Result<(), RequestError> {
69 if self.deadline <= now {
70 Err(RequestError::DeadlineElapsed)
71 } else {
72 Ok(())
73 }
74 }
75
76 #[must_use]
78 pub fn source_host(&self) -> &HostId {
79 &self.source_host
80 }
81
82 #[must_use]
84 pub fn source_path(&self) -> &Path {
85 &self.source_path
86 }
87
88 #[must_use]
90 pub fn destination_host(&self) -> &HostId {
91 &self.destination_host
92 }
93
94 #[must_use]
96 pub fn destination_path(&self) -> &Path {
97 &self.destination_path
98 }
99
100 #[must_use]
102 pub const fn max_bytes(&self) -> u64 {
103 self.max_bytes
104 }
105
106 #[must_use]
108 pub const fn deadline(&self) -> Timestamp {
109 self.deadline
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct TransferReceipt {
116 bytes: u64,
117 source_sha256: Option<String>,
118 destination_sha256: Option<String>,
119}
120
121impl TransferReceipt {
122 #[must_use]
124 pub const fn new(bytes: u64) -> Self {
125 Self {
126 bytes,
127 source_sha256: None,
128 destination_sha256: None,
129 }
130 }
131
132 pub fn with_digests(
134 mut self,
135 source: impl Into<String>,
136 destination: impl Into<String>,
137 ) -> Result<Self, RequestError> {
138 let source = source.into();
139 let destination = destination.into();
140 validate_sha256(&source)?;
141 validate_sha256(&destination)?;
142 self.source_sha256 = Some(source);
143 self.destination_sha256 = Some(destination);
144 Ok(self)
145 }
146
147 #[must_use]
149 pub const fn bytes(&self) -> u64 {
150 self.bytes
151 }
152
153 #[must_use]
155 pub fn source_sha256(&self) -> Option<&str> {
156 self.source_sha256.as_deref()
157 }
158
159 #[must_use]
161 pub fn destination_sha256(&self) -> Option<&str> {
162 self.destination_sha256.as_deref()
163 }
164
165 #[must_use]
167 pub fn verified(&self) -> bool {
168 matches!(
169 (&self.source_sha256, &self.destination_sha256),
170 (Some(source), Some(destination)) if source == destination
171 )
172 }
173}
174
175fn validate_sha256(value: &str) -> Result<(), RequestError> {
176 if value.len() == 64
177 && value
178 .bytes()
179 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
180 {
181 Ok(())
182 } else {
183 Err(RequestError::InvalidSha256)
184 }
185}
186#[cfg(test)]
187#[path = "transfer_tests.rs"]
188mod tests;