z_osmf/files/
list.rs

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// TODO: impl serde?
158#[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}