1use std::collections::{HashMap, HashSet};
11use std::fs;
12use std::io::{Read, Write};
13use std::path::{Path, PathBuf};
14
15use flate2::Compression;
16use flate2::read::GzDecoder;
17use flate2::write::GzEncoder;
18
19use crate::error::Result;
20
21#[derive(Debug, Clone)]
23pub struct Storage {
24 datadir: PathBuf,
25}
26
27fn gzip(data: &[u8]) -> std::io::Result<Vec<u8>> {
28 let mut enc = GzEncoder::new(Vec::new(), Compression::default());
29 enc.write_all(data)?;
30 enc.finish()
31}
32
33fn gunzip(data: &[u8]) -> std::io::Result<Vec<u8>> {
34 let mut dec = GzDecoder::new(data);
35 let mut out = Vec::new();
36 dec.read_to_end(&mut out)?;
37 Ok(out)
38}
39
40impl Storage {
41 pub fn new() -> Result<Self> {
44 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
45 let default_dir = home.join("tse-cache");
46 let tracker = home.join(".tse");
47
48 let datadir = if tracker.exists() {
49 let recorded = fs::read_to_string(&tracker).unwrap_or_default();
50 let p = PathBuf::from(recorded.trim());
51 if p.is_dir() { p } else { default_dir.clone() }
52 } else {
53 if !default_dir.exists() {
54 fs::create_dir_all(&default_dir)?;
55 }
56 fs::write(&tracker, default_dir.to_string_lossy().as_bytes())?;
57 default_dir.clone()
58 };
59
60 if !datadir.exists() {
61 fs::create_dir_all(&datadir)?;
62 }
63 Ok(Storage { datadir })
64 }
65
66 pub fn with_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
68 let datadir = dir.as_ref().to_path_buf();
69 fs::create_dir_all(&datadir)?;
70 Ok(Storage { datadir })
71 }
72
73 pub fn cache_dir(&self) -> &Path {
75 &self.datadir
76 }
77
78 fn resolve(&self, key: &str) -> PathBuf {
79 let key = key.strip_prefix("tse.").unwrap_or(key);
80 if let Some(rest) = key.strip_prefix("prices.") {
81 self.datadir.join("prices").join(format!("{rest}.csv"))
82 } else {
83 self.datadir.join(format!("{key}.csv"))
84 }
85 }
86
87 pub fn get_item(&self, key: &str) -> Result<String> {
89 let file = self.resolve(key);
90 if let Some(parent) = file.parent() {
91 if !parent.exists() {
92 fs::create_dir_all(parent)?;
93 }
94 }
95 if !file.exists() {
96 fs::write(&file, b"")?;
97 return Ok(String::new());
98 }
99 Ok(fs::read_to_string(&file)?)
100 }
101
102 pub fn set_item(&self, key: &str, value: &str) -> Result<()> {
104 let file = self.resolve(key);
105 if let Some(parent) = file.parent() {
106 if !parent.exists() {
107 fs::create_dir_all(parent)?;
108 }
109 }
110 fs::write(&file, value.as_bytes())?;
111 Ok(())
112 }
113
114 fn resolve_zipped(&self, key: &str, zip: bool) -> PathBuf {
115 let mut p = self.resolve(key);
116 if zip {
117 let name = format!("{}.gz", p.file_name().unwrap().to_string_lossy());
118 p.set_file_name(name);
119 }
120 p
121 }
122
123 pub fn get_item_async(&self, key: &str, zip: bool) -> Result<String> {
125 let file = self.resolve_zipped(key, zip);
126 if let Some(parent) = file.parent() {
127 if !parent.exists() {
128 fs::create_dir_all(parent)?;
129 }
130 }
131 if !file.exists() {
132 if !zip {
133 fs::write(&file, b"")?;
134 }
135 return Ok(String::new());
136 }
137 let raw = fs::read(&file)?;
138 if zip {
139 Ok(String::from_utf8_lossy(&gunzip(&raw)?).into_owned())
140 } else {
141 Ok(String::from_utf8_lossy(&raw).into_owned())
142 }
143 }
144
145 pub fn set_item_async(&self, key: &str, value: &str, zip: bool) -> Result<()> {
147 let file = self.resolve_zipped(key, zip);
148 if let Some(parent) = file.parent() {
149 if !parent.exists() {
150 fs::create_dir_all(parent)?;
151 }
152 }
153 if zip {
154 fs::write(&file, gzip(value.as_bytes())?)?;
155 } else {
156 fs::write(&file, value.as_bytes())?;
157 }
158 Ok(())
159 }
160
161 pub fn get_items(
164 &self,
165 selins: &HashSet<String>,
166 result: &mut HashMap<String, String>,
167 ) -> Result<()> {
168 let dir = self.datadir.join("prices");
169 if !dir.exists() {
170 fs::create_dir_all(&dir)?;
171 return Ok(());
172 }
173 for entry in fs::read_dir(&dir)? {
174 let entry = entry?;
175 let name = entry.file_name().to_string_lossy().into_owned();
176 let key = name.strip_suffix(".csv").unwrap_or(&name).to_string();
177 if !selins.contains(&key) {
178 continue;
179 }
180 result.insert(key, fs::read_to_string(entry.path())?);
181 }
182 Ok(())
183 }
184
185 pub fn itd_get_items(
189 &self,
190 selins: &HashSet<String>,
191 full: bool,
192 ) -> Result<HashMap<String, HashMap<String, ItdValue>>> {
193 let dir = self.datadir.join("intraday");
194 if !dir.exists() {
195 fs::create_dir_all(&dir)?;
196 return Ok(HashMap::new());
197 }
198 let mut result: HashMap<String, HashMap<String, ItdValue>> = HashMap::new();
199 for entry in fs::read_dir(&dir)? {
200 let entry = entry?;
201 if !entry.path().is_dir() {
202 continue;
203 }
204 let inscode = entry.file_name().to_string_lossy().into_owned();
205 if !selins.contains(&inscode) {
206 continue;
207 }
208 let mut files: HashMap<String, ItdValue> = HashMap::new();
209 for f in fs::read_dir(entry.path())? {
210 let f = f?;
211 let fname = f.file_name().to_string_lossy().into_owned();
212 let (deven, zipped) = match fname.strip_suffix(".gz") {
213 Some(stem) => (stem.to_string(), true),
214 None => (fname.clone(), false),
215 };
216 if full {
217 let raw = fs::read(f.path())?;
218 let val = if zipped {
219 ItdValue::Gzipped(raw)
220 } else {
221 ItdValue::Plain(String::from_utf8_lossy(&raw).into_owned())
222 };
223 files.insert(deven, val);
224 } else {
225 files.insert(deven, ItdValue::Present);
226 }
227 }
228 result.insert(inscode, files);
229 }
230 Ok(result)
231 }
232
233 pub fn itd_set_item(&self, key: &str, obj: &HashMap<String, ItdValue>) -> Result<()> {
236 let key = key.strip_prefix("tse.").unwrap_or(key);
237 let dir = self.datadir.join("intraday").join(key);
238 if !dir.exists() {
239 fs::create_dir_all(&dir)?;
240 }
241 for (deven, val) in obj {
242 match val {
243 ItdValue::Plain(s) if s == "N/A" => {
244 fs::write(dir.join(deven), s.as_bytes())?;
245 }
246 ItdValue::Plain(s) => {
247 fs::write(dir.join(format!("{deven}.gz")), gzip(s.as_bytes())?)?;
248 }
249 ItdValue::Gzipped(bytes) => {
250 fs::write(dir.join(format!("{deven}.gz")), bytes)?;
251 }
252 ItdValue::Present => {}
253 }
254 }
255 Ok(())
256 }
257}
258
259#[derive(Debug, Clone)]
262pub enum ItdValue {
263 Present,
264 Plain(String),
265 Gzipped(Vec<u8>),
266}
267
268impl ItdValue {
269 pub fn to_text(&self) -> Option<String> {
271 match self {
272 ItdValue::Plain(s) => Some(s.clone()),
273 ItdValue::Gzipped(b) => gunzip(b)
274 .ok()
275 .map(|v| String::from_utf8_lossy(&v).into_owned()),
276 ItdValue::Present => None,
277 }
278 }
279}