1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use chrono::NaiveDateTime;
5use serde::{Deserialize, Serialize};
6use z_osmf_macros::{Endpoint, Getters};
7
8use crate::convert::TryFromResponse;
9use crate::restfiles::get_transaction_id;
10use crate::{ClientCore, Result};
11
12#[derive(Clone, Debug, Deserialize, Eq, Getters, Hash, Ord, PartialEq, PartialOrd, Serialize)]
13pub struct FileAttributes {
14 name: Arc<str>,
15 mode: Option<Arc<str>>,
16 #[getter(copy)]
17 size: Option<i32>,
18 #[getter(copy)]
19 uid: Option<i32>,
20 #[serde(default)]
21 user: Option<Arc<str>>,
22 #[getter(copy)]
23 gid: Option<i32>,
24 group: Option<Arc<str>>,
25 #[getter(copy)]
26 mtime: Option<NaiveDateTime>,
27 #[serde(default)]
28 target: Option<Arc<str>>,
29}
30
31#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub enum FileFilter<T>
33where
34 T: std::fmt::Display + std::str::FromStr,
35 <T as std::str::FromStr>::Err: std::fmt::Display,
36{
37 LessThan(T),
38 EqualTo(T),
39 GreaterThan(T),
40}
41
42impl<'de, T> Deserialize<'de> for FileFilter<T>
43where
44 T: std::fmt::Display + std::str::FromStr,
45 <T as std::str::FromStr>::Err: std::fmt::Display,
46{
47 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
48 where
49 D: serde::Deserializer<'de>,
50 {
51 let s = String::deserialize(deserializer)?;
52
53 let v = match s {
54 s if s.starts_with('+') => FileFilter::GreaterThan(
55 T::from_str(s.trim_start_matches('+')).map_err(serde::de::Error::custom)?,
56 ),
57 s if s.starts_with('-') => FileFilter::LessThan(
58 T::from_str(s.trim_start_matches('-')).map_err(serde::de::Error::custom)?,
59 ),
60 s => FileFilter::EqualTo(T::from_str(&s).map_err(serde::de::Error::custom)?),
61 };
62
63 Ok(v)
64 }
65}
66
67impl<T> Serialize for FileFilter<T>
68where
69 T: std::fmt::Display + std::str::FromStr,
70 <T as std::str::FromStr>::Err: std::fmt::Display,
71{
72 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
73 where
74 S: serde::Serializer,
75 {
76 let s = match self {
77 FileFilter::EqualTo(f) => format!("{}", f),
78 FileFilter::GreaterThan(f) => format!("+{}", f),
79 FileFilter::LessThan(f) => format!("-{}", f),
80 };
81
82 serializer.serialize_str(&s)
83 }
84}
85
86#[derive(Clone, Debug, Deserialize, Eq, Getters, Hash, Ord, PartialEq, PartialOrd, Serialize)]
87pub struct FileList {
88 items: Arc<[FileAttributes]>,
89 #[getter(copy)]
90 returned_rows: i32,
91 #[getter(copy)]
92 total_rows: i32,
93 #[getter(copy)]
94 json_version: i32,
95 transaction_id: Arc<str>,
96}
97
98impl TryFromResponse for FileList {
99 async fn try_from_response(value: reqwest::Response) -> Result<Self> {
100 let transaction_id = get_transaction_id(&value)?;
101
102 let ResponseJson {
103 items,
104 returned_rows,
105 total_rows,
106 json_version,
107 } = value.json().await?;
108
109 Ok(FileList {
110 items,
111 returned_rows,
112 total_rows,
113 json_version,
114 transaction_id,
115 })
116 }
117}
118
119#[derive(Clone, Debug, Endpoint)]
120#[endpoint(method = get, path = "/zosmf/restfiles/fs")]
121pub struct FileListBuilder<T>
122where
123 T: TryFromResponse,
124{
125 core: Arc<ClientCore>,
126
127 #[endpoint(query = "path")]
128 path: Arc<str>,
129 #[endpoint(builder_fn = build_lstat)]
130 lstat: Option<bool>,
131 #[endpoint(query = "group")]
132 group: Option<Arc<str>>,
133 #[endpoint(query = "mtime")]
134 modified_days: Option<FileFilter<u32>>,
135 #[endpoint(query = "name")]
136 name: Option<Arc<str>>,
137 #[endpoint(query = "size")]
138 size: Option<FileFilter<FileSize>>,
139 #[endpoint(query = "perm")]
140 permissions: Option<Arc<str>>,
141 #[endpoint(query = "type")]
142 file_type: Option<FileType>,
143 #[endpoint(query = "user")]
144 user: Option<Arc<str>>,
145 #[endpoint(query = "depth")]
146 depth: Option<i32>,
147 #[endpoint(query = "limit")]
148 limit: Option<i32>,
149 #[endpoint(query = "filesys")]
150 file_system: Option<FileSystem>,
151 #[endpoint(query = "symlinks")]
152 symlinks: Option<FileSymLinks>,
153
154 target_type: PhantomData<T>,
155}
156
157#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
159pub enum FileSize {
160 Bytes(u32),
161 Kilobytes(u32),
162 Megabytes(u32),
163 Gigabytes(u32),
164}
165
166impl std::fmt::Display for FileSize {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 FileSize::Bytes(s) => write!(f, "{}", s),
170 FileSize::Kilobytes(s) => write!(f, "{}K", s),
171 FileSize::Megabytes(s) => write!(f, "{}M", s),
172 FileSize::Gigabytes(s) => write!(f, "{}G", s),
173 }
174 }
175}
176
177impl std::str::FromStr for FileSize {
178 type Err = std::num::ParseIntError;
179
180 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
181 let v = match s {
182 s if s.ends_with('K') => FileSize::Kilobytes(u32::from_str(s.trim_end_matches('K'))?),
183 s if s.ends_with('M') => FileSize::Megabytes(u32::from_str(s.trim_end_matches('M'))?),
184 s if s.ends_with('G') => FileSize::Gigabytes(u32::from_str(s.trim_end_matches('G'))?),
185 s => FileSize::Bytes(u32::from_str(s)?),
186 };
187
188 Ok(v)
189 }
190}
191
192#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
193#[serde(rename_all = "lowercase")]
194pub enum FileSymLinks {
195 Follow,
196 Report,
197}
198
199#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
200#[serde(rename_all = "lowercase")]
201pub enum FileSystem {
202 All,
203 Same,
204}
205
206#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
207pub enum FileType {
208 #[serde(rename = "c")]
209 CharacterSpecialFile,
210 #[serde(rename = "d")]
211 Directory,
212 #[serde(rename = "p")]
213 FIFO,
214 #[serde(rename = "f")]
215 File,
216 #[serde(rename = "s")]
217 Socket,
218 #[serde(rename = "l")]
219 SymbolicLink,
220}
221
222#[derive(Clone, Debug, Deserialize, Serialize)]
223#[serde(rename_all = "camelCase")]
224struct ResponseJson {
225 items: Arc<[FileAttributes]>,
226 returned_rows: i32,
227 total_rows: i32,
228 #[serde(rename = "JSONversion")]
229 json_version: i32,
230}
231
232fn build_lstat<T>(
233 request_builder: reqwest::RequestBuilder,
234 builder: &FileListBuilder<T>,
235) -> reqwest::RequestBuilder
236where
237 T: TryFromResponse,
238{
239 match builder.lstat {
240 Some(true) => request_builder.header("X-IBM-Lstat", "true"),
241 _ => request_builder,
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use crate::tests::get_zosmf;
248
249 use super::*;
250
251 #[test]
252 fn example_1() {
253 let zosmf = get_zosmf();
254
255 let manual_request = zosmf
256 .core
257 .client
258 .get("https://test.com/zosmf/restfiles/fs")
259 .query(&[("path", "/usr")])
260 .build()
261 .unwrap();
262
263 let list_files = zosmf.files().list("/usr").get_request().unwrap();
264
265 assert_eq!(format!("{:?}", manual_request), format!("{:?}", list_files))
266 }
267
268 #[test]
269 fn example_2() {
270 let zosmf = get_zosmf();
271
272 let manual_request = zosmf
273 .core
274 .client
275 .get("https://test.com/zosmf/restfiles/fs")
276 .query(&[("path", "/u/ibmuser/myFile.txt")])
277 .build()
278 .unwrap();
279
280 let list_files = zosmf
281 .files()
282 .list("/u/ibmuser/myFile.txt")
283 .get_request()
284 .unwrap();
285
286 assert_eq!(format!("{:?}", manual_request), format!("{:?}", list_files))
287 }
288
289 #[test]
290 fn example_3() {
291 let zosmf = get_zosmf();
292
293 let manual_request = zosmf
294 .core
295 .client
296 .get("https://test.com/zosmf/restfiles/fs")
297 .query(&[("path", "/usr/include"), ("name", "f*.h")])
298 .build()
299 .unwrap();
300
301 let list_files = zosmf
302 .files()
303 .list("/usr/include")
304 .name("f*.h")
305 .get_request()
306 .unwrap();
307
308 assert_eq!(format!("{:?}", manual_request), format!("{:?}", list_files))
309 }
310
311 #[test]
312 fn maximal_request() {
313 let zosmf = get_zosmf();
314
315 let manual_request = zosmf
316 .core
317 .client
318 .get("https://test.com/zosmf/restfiles/fs")
319 .query(&[
320 ("path", "/usr/include"),
321 ("group", "ibmgrp"),
322 ("mtime", "1"),
323 ("name", "f*.h"),
324 ("size", "10K"),
325 ("perm", "755"),
326 ("type", "f"),
327 ("user", "ibmuser"),
328 ("depth", "5"),
329 ("limit", "100"),
330 ("filesys", "all"),
331 ("symlinks", "follow"),
332 ])
333 .header("X-IBM-Lstat", "true")
334 .build()
335 .unwrap();
336
337 let request = zosmf
338 .files()
339 .list("/usr/include")
340 .name("f*.h")
341 .depth(5)
342 .file_system(FileSystem::All)
343 .file_type(FileType::File)
344 .group("ibmgrp")
345 .limit(100)
346 .lstat(true)
347 .modified_days(FileFilter::EqualTo(1))
348 .permissions("755")
349 .size(FileFilter::EqualTo(FileSize::Kilobytes(10)))
350 .symlinks(FileSymLinks::Follow)
351 .user("ibmuser")
352 .get_request()
353 .unwrap();
354
355 assert_eq!(format!("{:?}", manual_request), format!("{:?}", request))
356 }
357}