bom_buddy/
ftp.rs

1use crate::radar::{
2    Radar, RadarData, RadarId, RadarImageFeature, RadarImageFeatureLayer, RadarImageLegend,
3    RadarLegendType, RadarType,
4};
5use anyhow::{anyhow, Result};
6use chrono::Duration;
7use std::str::FromStr;
8use std::{io::Cursor, thread::sleep};
9use strum::IntoEnumIterator;
10use suppaftp::list::File;
11use suppaftp::FtpStream;
12use tracing::{debug, error};
13
14pub struct FtpClient {
15    // Store FtpStream as an Option to avoid login delay when FtpClient is constructed
16    ftp_stream: Option<FtpStream>,
17    root_url: String,
18}
19
20impl FtpClient {
21    pub fn new() -> Result<Self> {
22        let root_url = "ftp.bom.gov.au:21".to_string();
23        Ok(FtpClient {
24            ftp_stream: None,
25            root_url,
26        })
27    }
28
29    fn stream(&mut self) -> Result<&mut FtpStream> {
30        if self.ftp_stream.is_none() {
31            let mut stream = FtpStream::connect(&self.root_url)?;
32            stream.login("anonymous", "guest")?;
33            self.ftp_stream = Some(stream);
34        }
35        Ok(self.ftp_stream.as_mut().unwrap())
36    }
37
38    fn get_buf(&mut self, path: &str) -> Result<Cursor<Vec<u8>>> {
39        debug!("Downloading {}{path}", self.root_url);
40        Ok(self.stream()?.retr_as_buffer(path)?)
41    }
42
43    pub fn keepalive(&mut self) -> Result<()> {
44        if self.ftp_stream.is_some() {
45            self.stream()?.noop()?;
46        }
47        Ok(())
48    }
49
50    pub fn list_files(&mut self, path: &str) -> Result<impl Iterator<Item = File>> {
51        let url = format!("{}{}", self.root_url, path);
52        debug!("Listing directory {url}");
53        let mut attempts = 0;
54        let listing = loop {
55            match self.stream()?.list(Some(path)) {
56                Ok(l) => break l,
57                Err(e) => {
58                    error!("Error listing directory {url}. {e} Retry in 5 seconds");
59                    attempts += 1;
60                    if attempts > 5 {
61                        return Err(anyhow!(
62                            "Failed to list directory {url} after {attempts} attempts. {e}"
63                        ));
64                    }
65                    sleep(Duration::seconds(5).to_std().unwrap());
66                    continue;
67                }
68            };
69        };
70        Ok(listing.into_iter().map(|s| File::from_str(&s).unwrap()))
71    }
72
73    pub fn get_radar_data(&mut self) -> Result<Vec<RadarData>> {
74        let buf = self.get_buf("/anon/home/adfd/spatial/IDR00007.dbf")?;
75        let mut reader = dbase::Reader::new(buf)?;
76        Ok(reader.read_as::<RadarData>()?)
77    }
78
79    pub fn get_public_radars(&mut self) -> Result<impl Iterator<Item = Radar>> {
80        Ok(self
81            .get_radar_data()?
82            .into_iter()
83            .filter(|r| r.status == "Public")
84            .map(|r| r.into()))
85    }
86
87    pub fn get_radar_legends(&mut self) -> Result<Vec<RadarImageLegend>> {
88        let mut legends = Vec::with_capacity(3);
89        for t in RadarLegendType::iter() {
90            let path = format!("/anon/gen/radar_transparencies/IDR.legend.{}.png", t.id());
91            legends.push(RadarImageLegend {
92                r#type: t,
93                png_buf: self.get_buf(&path)?.into_inner(),
94            })
95        }
96        Ok(legends)
97    }
98
99    pub fn get_radar_feature_layers(
100        &mut self,
101        id: RadarId,
102        size: RadarType,
103    ) -> Result<Vec<RadarImageFeatureLayer>> {
104        let mut layers = Vec::new();
105        for feature in RadarImageFeature::iter() {
106            let layer = self.get_radar_feature_layer(id, size, feature)?;
107            layers.push(layer);
108        }
109        Ok(layers)
110    }
111
112    pub fn get_radar_feature_layer(
113        &mut self,
114        id: RadarId,
115        size: RadarType,
116        feature: RadarImageFeature,
117    ) -> Result<RadarImageFeatureLayer> {
118        let filename = format!("IDR{id:02}{}.{feature}.png", size.id());
119        let path = format!("/anon/gen/radar_transparencies/{filename}");
120        let png_buf = self.get_buf(&path)?.into_inner();
121        Ok(RadarImageFeatureLayer {
122            feature,
123            size,
124            radar_id: id,
125            png_buf,
126            filename,
127        })
128    }
129
130    pub fn list_radar_data_layers(&mut self) -> Result<impl Iterator<Item = File>> {
131        Ok(self
132            .list_files("/anon/gen/radar")?
133            .filter(move |f| f.name().ends_with(".png")))
134    }
135
136    pub fn get_radar_data_png(&mut self, filename: &str) -> Result<Vec<u8>> {
137        let buf = self.get_buf(&format!("/anon/gen/radar/{}", filename))?;
138        Ok(buf.into_inner())
139    }
140}