heisenberg_data_processing/raw/
fetch.rs1use std::{fs, path::Path};
2
3use futures::StreamExt;
4use indicatif::{ProgressBar, ProgressStyle};
5use reqwest::Client;
6use tempfile::NamedTempFile;
7use tokio::io::AsyncWriteExt;
8use tracing::{info, instrument};
9use zip::ZipArchive;
10
11use super::Result;
12use crate::{
13 DataError, DataSource,
14 raw::{
15 admin::{Admin1CodeRawData, Admin2CodeRawData},
16 country_info::CountryInfoRawData,
17 feature_codes::FeatureCodesRawData,
18 places::PlacesRawData,
19 },
20};
21
22pub struct TempData {
23 pub places: PlacesRawData,
24 pub country_info: CountryInfoRawData,
25 pub admin1: Admin1CodeRawData,
26 pub admin2: Admin2CodeRawData,
27 pub feature_codes: FeatureCodesRawData,
28}
29impl TempData {
30 #[must_use]
31 pub fn from_temp_files(
32 places: NamedTempFile,
33 country_info: NamedTempFile,
34 admin1: NamedTempFile,
35 admin2: NamedTempFile,
36 feature_codes: NamedTempFile,
37 ) -> Self {
38 Self {
39 places: PlacesRawData::new(places),
40 country_info: CountryInfoRawData::new(country_info),
41 admin1: Admin1CodeRawData::new(admin1),
42 admin2: Admin2CodeRawData::new(admin2),
43 feature_codes: FeatureCodesRawData::new(feature_codes),
44 }
45 }
46}
47
48#[instrument(name = "Download data", skip_all, level = "info")]
49pub fn download_data(data_source: &DataSource) -> Result<TempData> {
50 let data_source_url = data_source
51 .places_url()
52 .ok_or_else(|| DataError::NoDataDirProvided)?;
53
54 tokio::runtime::Runtime::new()?.block_on(async {
55 let client = Client::new();
56
57 let (places, country_info, admin1, admin2, feature_codes) = tokio::try_join!(
58 download_places_data(&client, &data_source_url),
59 download_country_info(&client),
60 download_admin1_codes(&client),
61 download_admin2_codes(&client),
62 download_feature_codes(&client),
63 )?;
64
65 Ok(TempData::from_temp_files(
66 places,
67 country_info,
68 admin1,
69 admin2,
70 feature_codes,
71 ))
72 })
73}
74
75async fn download_places_data(client: &Client, url: &str) -> Result<NamedTempFile> {
99 download_zip_and_extract_first_entry_to_temp_file(client, url).await
100}
101
102const COUNTRY_INFO_URL: &str = "https://download.geonames.org/export/dump/countryInfo.txt";
103async fn download_country_info(client: &Client) -> Result<NamedTempFile> {
105 download_to_temp_file(client, COUNTRY_INFO_URL).await
106}
107
108const FEATURE_CODES_URL: &str = "https://download.geonames.org/export/dump/featureCodes_en.txt";
109async fn download_feature_codes(client: &Client) -> Result<NamedTempFile> {
111 download_to_temp_file(client, FEATURE_CODES_URL).await
112}
113
114const ADMIN1_CODES_URL: &str = "https://download.geonames.org/export/dump/admin1CodesASCII.txt";
115async fn download_admin1_codes(client: &Client) -> Result<NamedTempFile> {
117 download_to_temp_file(client, ADMIN1_CODES_URL).await
118}
119
120const ADMIN2_CODES_URL: &str = "https://download.geonames.org/export/dump/admin2Codes.txt";
121async fn download_admin2_codes(client: &Client) -> Result<NamedTempFile> {
123 download_to_temp_file(client, ADMIN2_CODES_URL).await
124}
125
126async fn download_to_temp_file(client: &Client, url: &str) -> Result<NamedTempFile> {
127 info!(url, "Starting download");
128 let response = client.get(url).send().await?.error_for_status()?;
129
130 let total_size = response.content_length().unwrap_or(0);
131
132 let pb = ProgressBar::new(total_size);
133 pb.set_style(ProgressStyle::default_bar()
134 .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").expect("Progress bar template")
135 .progress_chars("█░"));
136 pb.set_message(format!(
137 "Downloading {}",
138 url.split('/').next_back().unwrap_or(url)
139 ));
140
141 let temp_file = NamedTempFile::new()?;
142 let mut dest_file = tokio::fs::File::create(temp_file.path()).await?;
143 let mut stream = response.bytes_stream();
146 while let Some(item) = stream.next().await {
147 let chunk = item?;
148 dest_file.write_all(&chunk).await?;
149 pb.inc(chunk.len() as u64);
150 }
151 dest_file.flush().await?; pb.finish_and_clear();
153 println!();
154 Ok(temp_file)
155}
156
157async fn download_zip_and_extract_first_entry_to_temp_file(
158 client: &Client,
159 zip_url: &str,
160) -> Result<NamedTempFile> {
161 info!(zip_url, "Starting ZIP download");
162 let zip_temp_file = download_to_temp_file(client, zip_url).await?;
164 info!(path = ?zip_temp_file.path(), "ZIP download complete");
165
166 let zip_file_path = zip_temp_file.path().to_path_buf();
167
168 let extracted_content_temp_file =
169 tokio::task::spawn_blocking(move || extract_first_entry_from_zip(zip_file_path)).await??;
170
171 Ok(extracted_content_temp_file)
172}
173
174fn extract_first_entry_from_zip(zip_file_path: impl AsRef<Path>) -> Result<NamedTempFile> {
175 let zip_fs_file = fs::File::open(&zip_file_path)?;
176 let mut archive = ZipArchive::new(zip_fs_file)?;
177
178 if archive.is_empty() {
179 return Err(zip::result::ZipError::FileNotFound.into());
180 }
181
182 let mut file_in_zip = archive.by_index(0)?;
183
184 let extracted_content_temp_file = NamedTempFile::with_suffix(".txt")?;
185 let mut extracted_fs_file = fs::File::create(extracted_content_temp_file.path())?;
186
187 std::io::copy(&mut file_in_zip, &mut extracted_fs_file)?;
188 info!(path = ?extracted_content_temp_file.path(), "File extracted successfully");
189
190 Ok(extracted_content_temp_file)
191}