Skip to main content

strop_remote/
client.rs

1//! Worker-only requests to the per-endpoint SFTP actor. Cloning a client or
2//! lease never connects; every accepted request retains its own lease.
3use crate::address::{RemoteEndpoint, RemoteFile, RemoteLocation};
4use crate::pool::{EndpointHandle, Job, Registry, Reply, Request, SessionState};
5use crate::{ReadFailureKind, ReadSelection, ReadStage, RemoteReadError, RemoteWindow};
6use std::sync::{mpsc, Arc};
7use std::time::Duration;
8use strop_core::worker::CancelToken;
9
10const CANCEL_POLL: Duration = Duration::from_millis(50);
11
12#[derive(Debug, Clone)]
13pub struct ConnectionLease {
14    pub(crate) inner: Arc<EndpointHandle>,
15}
16impl ConnectionLease {
17    pub fn endpoint(&self) -> &RemoteEndpoint {
18        self.inner.endpoint()
19    }
20    pub fn is_connected(&self) -> bool {
21        !self.inner.stop_signal().signalled() && self.inner.status() == SessionState::Connected
22    }
23}
24
25pub struct RemoteSnapshot {
26    pub file: RemoteFile,
27    pub buffer: strop_core::Buffer,
28    pub window: RemoteWindow,
29    pub connection: ConnectionLease,
30}
31pub struct RemoteDirectorySnapshot {
32    pub directory: RemoteFile,
33    pub entries: Vec<RemoteEntry>,
34    pub connection: ConnectionLease,
35}
36pub enum RemoteResource {
37    File(Box<RemoteSnapshot>),
38    Directory(RemoteDirectorySnapshot),
39}
40#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
41pub struct RemoteEntry {
42    pub file: RemoteFile,
43    pub kind: RemoteEntryKind,
44    pub permissions: Option<RemotePermissions>,
45    /// Server-reported bytes, never a recursively computed directory total.
46    pub size: Option<crate::RemoteSize>,
47}
48#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub enum RemoteEntryKind {
50    File,
51    Directory,
52    SymbolicLink,
53    Fifo,
54    Socket,
55    BlockDevice,
56    CharacterDevice,
57    Unknown,
58}
59
60impl RemoteEntryKind {
61    pub const fn marker(self) -> char {
62        match self {
63            Self::File => '-',
64            Self::Directory => 'd',
65            Self::SymbolicLink => 'l',
66            Self::Fifo => 'p',
67            Self::Socket => 's',
68            Self::BlockDevice => 'b',
69            Self::CharacterDevice => 'c',
70            Self::Unknown => '?',
71        }
72    }
73}
74
75/// Only the POSIX access/special bits; file kind is a separate typed value.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
77#[serde(try_from = "u16", into = "u16")]
78pub struct RemotePermissions(u16);
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
81#[error("permission bits exceed POSIX access and special bits")]
82pub struct PermissionBitsError;
83
84impl RemotePermissions {
85    pub const fn new(bits: u16) -> Result<Self, PermissionBitsError> {
86        if bits & !0o7777 != 0 {
87            Err(PermissionBitsError)
88        } else {
89            Ok(Self(bits))
90        }
91    }
92    pub(crate) const fn from_mode(mode: u32) -> Self {
93        Self((mode & 0o7777) as u16)
94    }
95    pub const fn bits(self) -> u16 {
96        self.0
97    }
98}
99impl TryFrom<u16> for RemotePermissions {
100    type Error = PermissionBitsError;
101    fn try_from(bits: u16) -> Result<Self, Self::Error> {
102        Self::new(bits)
103    }
104}
105impl From<RemotePermissions> for u16 {
106    fn from(value: RemotePermissions) -> Self {
107        value.bits()
108    }
109}
110impl std::fmt::Display for RemotePermissions {
111    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        for (read, write, execute, special, lower, upper) in [
113            (0o400, 0o200, 0o100, 0o4000, 's', 'S'),
114            (0o040, 0o020, 0o010, 0o2000, 's', 'S'),
115            (0o004, 0o002, 0o001, 0o1000, 't', 'T'),
116        ] {
117            let r = if self.0 & read != 0 { 'r' } else { '-' };
118            let w = if self.0 & write != 0 { 'w' } else { '-' };
119            let x = match (self.0 & execute != 0, self.0 & special != 0) {
120                (true, true) => lower,
121                (false, true) => upper,
122                (true, false) => 'x',
123                (false, false) => '-',
124            };
125            write!(formatter, "{r}{w}{x}")?;
126        }
127        Ok(())
128    }
129}
130
131#[derive(Clone, Default)]
132pub struct RemoteClient {
133    registry: Arc<Registry>,
134}
135impl RemoteClient {
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    pub fn read(
141        &self,
142        location: &RemoteLocation,
143        selection: ReadSelection,
144        token: &CancelToken,
145    ) -> Result<RemoteSnapshot, RemoteReadError> {
146        match self.request(
147            location.endpoint(),
148            Request::Read {
149                location: location.clone(),
150                selection,
151            },
152            token,
153            false,
154        )? {
155            Reply::Snapshot(snapshot) => Ok(*snapshot),
156            _ => Err(wrong_reply()),
157        }
158    }
159    pub fn open(
160        &self,
161        location: &RemoteLocation,
162        selection: ReadSelection,
163        token: &CancelToken,
164    ) -> Result<RemoteResource, RemoteReadError> {
165        match self.request(
166            location.endpoint(),
167            Request::Open {
168                location: location.clone(),
169                selection,
170            },
171            token,
172            false,
173        )? {
174            Reply::Snapshot(snapshot) => Ok(RemoteResource::File(snapshot)),
175            Reply::Directory(snapshot) => Ok(RemoteResource::Directory(snapshot)),
176            _ => Err(wrong_reply()),
177        }
178    }
179    pub fn list(
180        &self,
181        location: &RemoteLocation,
182        token: &CancelToken,
183    ) -> Result<RemoteDirectorySnapshot, RemoteReadError> {
184        match self.request(
185            location.endpoint(),
186            Request::List {
187                location: location.clone(),
188            },
189            token,
190            false,
191        )? {
192            Reply::Directory(snapshot) => Ok(snapshot),
193            _ => Err(wrong_reply()),
194        }
195    }
196    /// Completion never creates an actor or authenticates. The actor checks the
197    /// same policy again when dequeuing, since the observed connection may die.
198    pub fn list_connected(
199        &self,
200        directory: &RemoteFile,
201        token: &CancelToken,
202    ) -> Result<Vec<RemoteEntry>, RemoteReadError> {
203        match self.request(
204            directory.endpoint(),
205            Request::ListConnected {
206                directory: directory.clone(),
207            },
208            token,
209            true,
210        )? {
211            Reply::Entries(entries) => Ok(entries),
212            _ => Err(wrong_reply()),
213        }
214    }
215    pub fn connect(
216        &self,
217        endpoint: &RemoteEndpoint,
218        token: &CancelToken,
219    ) -> Result<ConnectionLease, RemoteReadError> {
220        match self.request(endpoint, Request::Connect, token, false)? {
221            Reply::Lease(lease) => Ok(lease),
222            _ => Err(wrong_reply()),
223        }
224    }
225    /// Worker-only: revoke admission, then wait for the actor's cleanup result.
226    pub fn disconnect(&self, endpoint: &RemoteEndpoint) -> Result<(), RemoteReadError> {
227        if let Some(handle) = self.registry.remove(endpoint) {
228            handle.wait_stopped()?;
229        }
230        Ok(())
231    }
232    pub fn disconnect_all(&self) -> Result<(), RemoteReadError> {
233        let handles = self.registry.clear();
234        let mut failure = None;
235        for handle in handles {
236            if let Err(error) = handle.wait_stopped() {
237                failure.get_or_insert(error);
238            }
239        }
240        failure.map_or(Ok(()), Err)
241    }
242    /// Observational metadata, not a promise that the next request will succeed.
243    pub fn connections(&self) -> Vec<RemoteEndpoint> {
244        self.registry.connected()
245    }
246
247    fn request(
248        &self,
249        endpoint: &RemoteEndpoint,
250        request: Request,
251        cancel: &CancelToken,
252        connected_only: bool,
253    ) -> Result<Reply, RemoteReadError> {
254        if cancel.is_cancelled() {
255            return Err(cancelled());
256        }
257        let inner = if connected_only {
258            self.registry.connected_handle(endpoint).ok_or_else(|| {
259                RemoteReadError::bare(
260                    ReadStage::Session,
261                    ReadFailureKind::NotConnected,
262                    "completion requires an existing authenticated connection",
263                )
264            })?
265        } else {
266            self.registry.acquire(endpoint)?
267        };
268        let (reply, receiver) = mpsc::channel();
269        let job = Job {
270            lease: ConnectionLease {
271                inner: inner.clone(),
272            },
273            request,
274            cancel: cancel.clone(),
275            reply,
276        };
277        inner.jobs().try_send(job).map_err(|error| match error {
278            mpsc::TrySendError::Full(_) => RemoteReadError::bare(
279                ReadStage::Session,
280                ReadFailureKind::QueueFull,
281                "remote session queue is full",
282            ),
283            mpsc::TrySendError::Disconnected(_) => stopped(),
284        })?;
285        loop {
286            if cancel.is_cancelled() {
287                return Err(cancelled());
288            }
289            if inner.stop_signal().signalled() {
290                return Err(stopped());
291            }
292            match receiver.recv_timeout(CANCEL_POLL) {
293                Ok(result) => return result,
294                Err(mpsc::RecvTimeoutError::Timeout) => {}
295                Err(mpsc::RecvTimeoutError::Disconnected) => return Err(stopped()),
296            }
297        }
298    }
299}
300fn cancelled() -> RemoteReadError {
301    RemoteReadError::bare(
302        ReadStage::Session,
303        ReadFailureKind::Cancelled,
304        "remote request cancelled",
305    )
306}
307fn stopped() -> RemoteReadError {
308    RemoteReadError::bare(
309        ReadStage::Session,
310        ReadFailureKind::Stopped,
311        "remote session stopped before delivering a result",
312    )
313}
314fn wrong_reply() -> RemoteReadError {
315    RemoteReadError::bare(
316        ReadStage::Session,
317        ReadFailureKind::Protocol,
318        "session reply does not match the accepted request",
319    )
320}