1use std::path::{Path, PathBuf};
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use soma_fleet::{FileTransfer, HostId, HostRecord, TopologyRevision, TransferReceipt};
6use soma_ops::{MutationSendState, OperationId, OperationName, Timestamp};
7use tokio_util::sync::CancellationToken;
8
9use crate::{FileReadPolicy, InfraError, InfraResult};
10
11pub const MAX_FILE_TRANSFER_BYTES: u64 = 16 * 1024 * 1024;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct FileTransferPolicy {
17 source: FileReadPolicy,
18 destination: FileReadPolicy,
19}
20
21impl FileTransferPolicy {
22 pub fn new<SI, SP, DI, DP>(source_roots: SI, destination_roots: DI) -> InfraResult<Self>
24 where
25 SI: IntoIterator<Item = SP>,
26 SP: Into<PathBuf>,
27 DI: IntoIterator<Item = DP>,
28 DP: Into<PathBuf>,
29 {
30 Ok(Self {
31 source: FileReadPolicy::new(source_roots)?,
32 destination: FileReadPolicy::new(destination_roots)?,
33 })
34 }
35
36 #[cfg(any(feature = "process-driver", test))]
37 pub(crate) fn resolve_source(&self, path: &Path) -> InfraResult<(PathBuf, PathBuf)> {
38 ensure_named_file(self.source.resolve(path)?)
39 }
40
41 #[cfg(any(feature = "process-driver", test))]
42 pub(crate) fn resolve_destination(&self, path: &Path) -> InfraResult<(PathBuf, PathBuf)> {
43 ensure_named_file(self.destination.resolve(path)?)
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct TransferFileIdentity {
50 pub path: PathBuf,
52 pub bytes: u64,
54 pub sha256: String,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct FileTransferFingerprint {
61 pub source_host: HostId,
63 pub source_revision: TopologyRevision,
65 pub source: TransferFileIdentity,
67 pub destination_host: HostId,
69 pub destination_revision: TopologyRevision,
71 pub destination_path: PathBuf,
73 pub destination_before: Option<TransferFileIdentity>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct VerifiedFileTransferRequest {
80 pub operation_id: OperationId,
82 pub operation: OperationName,
84 pub fingerprint: FileTransferFingerprint,
86 pub deadline: Timestamp,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct VerifiedFileTransferOutcome {
93 pub before: FileTransferFingerprint,
95 pub destination_after: TransferFileIdentity,
97 pub bytes: u64,
99 pub send_state: MutationSendState,
101 pub verified: bool,
103 pub changed: bool,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum FileTransferPathRole {
110 Source,
112 Destination,
114}
115
116#[async_trait]
118pub trait FileTransferInspector: Send + Sync {
119 async fn inspect_transfer_file(
121 &self,
122 host: &HostRecord,
123 path: &Path,
124 role: FileTransferPathRole,
125 optional: bool,
126 cancellation: &CancellationToken,
127 ) -> InfraResult<Option<TransferFileIdentity>>;
128}
129
130pub trait VerifiedFileTransferClient: FileTransfer + FileTransferInspector {}
132impl<T> VerifiedFileTransferClient for T where T: FileTransfer + FileTransferInspector {}
133
134#[cfg(any(feature = "process-driver", test))]
135fn ensure_named_file((root, relative): (PathBuf, PathBuf)) -> InfraResult<(PathBuf, PathBuf)> {
136 if relative.as_os_str().is_empty() {
137 Err(InfraError::InvalidRequest {
138 domain: "file-transfer",
139 message: "transfer path must name a file beneath its configured root".into(),
140 })
141 } else {
142 Ok((root, relative))
143 }
144}
145
146#[cfg(any(feature = "process-driver", test))]
147pub(crate) fn identity_from_bytes(path: &Path, bytes: &[u8]) -> TransferFileIdentity {
148 TransferFileIdentity {
149 path: path.to_path_buf(),
150 bytes: bytes.len() as u64,
151 sha256: crate::mutation::sha256_hex(bytes),
152 }
153}
154
155pub(crate) fn receipt_identity(receipt: &TransferReceipt) -> InfraResult<(&str, &str)> {
156 let source = receipt
157 .source_sha256()
158 .ok_or_else(|| InfraError::InvalidRequest {
159 domain: "file-transfer",
160 message: "transfer receipt is missing source digest".into(),
161 })?;
162 let destination = receipt
163 .destination_sha256()
164 .ok_or_else(|| InfraError::InvalidRequest {
165 domain: "file-transfer",
166 message: "transfer receipt is missing destination digest".into(),
167 })?;
168 Ok((source, destination))
169}
170
171#[cfg(test)]
172#[path = "file_transfer_tests.rs"]
173mod tests;