1use std::path::{Path, PathBuf};
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use soma_fleet::{HostId, HostRecord, TopologyRevision};
6use soma_ops::Timestamp;
7use tokio_util::sync::CancellationToken;
8
9use crate::{FileKind, InfraError, InfraResult};
10
11const MAX_DEPTH: u8 = 20;
12const MAX_RESULTS: u32 = 500;
13const MAX_TAIL_LINES: u32 = 5000;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub struct PathReadRequest {
18 tree: bool,
19 depth: u8,
20 deadline: Timestamp,
21}
22
23impl PathReadRequest {
24 #[must_use]
26 pub const fn new(deadline: Timestamp) -> Self {
27 Self {
28 tree: false,
29 depth: 3,
30 deadline,
31 }
32 }
33 pub fn with_tree(mut self, depth: u8) -> InfraResult<Self> {
35 if depth == 0 || depth > MAX_DEPTH {
36 return Err(InfraError::InvalidRequest {
37 domain: "filesystem",
38 message: format!("tree depth must be 1-{MAX_DEPTH}"),
39 });
40 }
41 self.tree = true;
42 self.depth = depth;
43 Ok(self)
44 }
45 #[must_use]
47 pub const fn tree(&self) -> bool {
48 self.tree
49 }
50 #[must_use]
52 pub const fn depth(&self) -> u8 {
53 self.depth
54 }
55 #[must_use]
57 pub const fn deadline(&self) -> Timestamp {
58 self.deadline
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct PathRead {
65 pub host: HostId,
67 pub topology_revision: TopologyRevision,
69 pub path: PathBuf,
71 pub kind: FileKind,
73 pub content: Vec<u8>,
75 pub entries: Vec<String>,
77 pub size_bytes: u64,
79 pub truncated: bool,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct FileFindRequest {
86 pattern: String,
87 depth: u8,
88 limit: u32,
89 deadline: Timestamp,
90}
91
92impl FileFindRequest {
93 pub fn new(pattern: impl Into<String>, deadline: Timestamp) -> InfraResult<Self> {
95 let pattern = pattern.into();
96 validate_pattern(&pattern)?;
97 Ok(Self {
98 pattern,
99 depth: 10,
100 limit: MAX_RESULTS,
101 deadline,
102 })
103 }
104 pub fn with_depth(mut self, depth: u8) -> InfraResult<Self> {
106 if depth == 0 || depth > MAX_DEPTH {
107 return Err(InfraError::InvalidRequest {
108 domain: "filesystem",
109 message: format!("find depth must be 1-{MAX_DEPTH}"),
110 });
111 }
112 self.depth = depth;
113 Ok(self)
114 }
115 pub fn with_limit(mut self, limit: u32) -> InfraResult<Self> {
117 if limit == 0 || limit > MAX_RESULTS {
118 return Err(InfraError::InvalidRequest {
119 domain: "filesystem",
120 message: format!("find limit must be 1-{MAX_RESULTS}"),
121 });
122 }
123 self.limit = limit;
124 Ok(self)
125 }
126 #[must_use]
128 pub fn pattern(&self) -> &str {
129 &self.pattern
130 }
131 #[must_use]
133 pub const fn depth(&self) -> u8 {
134 self.depth
135 }
136 #[must_use]
138 pub const fn limit(&self) -> u32 {
139 self.limit
140 }
141 #[must_use]
143 pub const fn deadline(&self) -> Timestamp {
144 self.deadline
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct FileSearch {
151 pub host: HostId,
153 pub topology_revision: TopologyRevision,
155 pub path: PathBuf,
157 pub items: Vec<PathBuf>,
159 pub truncated: bool,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165pub struct FileTailRequest {
166 lines: u32,
167 deadline: Timestamp,
168}
169
170impl FileTailRequest {
171 #[must_use]
173 pub const fn new(deadline: Timestamp) -> Self {
174 Self {
175 lines: 100,
176 deadline,
177 }
178 }
179 pub fn with_lines(mut self, lines: u32) -> InfraResult<Self> {
181 if lines == 0 || lines > MAX_TAIL_LINES {
182 return Err(InfraError::InvalidRequest {
183 domain: "filesystem",
184 message: format!("tail lines must be 1-{MAX_TAIL_LINES}"),
185 });
186 }
187 self.lines = lines;
188 Ok(self)
189 }
190 #[must_use]
192 pub const fn lines(&self) -> u32 {
193 self.lines
194 }
195 #[must_use]
197 pub const fn deadline(&self) -> Timestamp {
198 self.deadline
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct FileTail {
205 pub host: HostId,
207 pub topology_revision: TopologyRevision,
209 pub path: PathBuf,
211 pub content: Vec<u8>,
213 pub line_count: usize,
215 pub truncated: bool,
217}
218
219#[async_trait]
221pub trait FilesystemQueryInspector: Send + Sync {
222 async fn read_path(
224 &self,
225 host: &HostRecord,
226 path: &Path,
227 request: &PathReadRequest,
228 cancellation: &CancellationToken,
229 ) -> InfraResult<PathRead>;
230 async fn find(
232 &self,
233 host: &HostRecord,
234 path: &Path,
235 request: &FileFindRequest,
236 cancellation: &CancellationToken,
237 ) -> InfraResult<FileSearch>;
238 async fn tail(
240 &self,
241 host: &HostRecord,
242 path: &Path,
243 request: &FileTailRequest,
244 cancellation: &CancellationToken,
245 ) -> InfraResult<FileTail>;
246}
247
248fn validate_pattern(value: &str) -> InfraResult<()> {
249 if value.is_empty()
250 || value.starts_with('-')
251 || value.chars().count() > 256
252 || value.chars().any(char::is_control)
253 {
254 Err(InfraError::InvalidRequest {
255 domain: "filesystem",
256 message: "invalid find pattern".into(),
257 })
258 } else {
259 Ok(())
260 }
261}
262
263#[cfg(test)]
264#[path = "filesystem_query_tests.rs"]
265mod tests;