use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use flate2::Compression;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct Storage {
datadir: PathBuf,
}
fn gzip(data: &[u8]) -> std::io::Result<Vec<u8>> {
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
enc.write_all(data)?;
enc.finish()
}
fn gunzip(data: &[u8]) -> std::io::Result<Vec<u8>> {
let mut dec = GzDecoder::new(data);
let mut out = Vec::new();
dec.read_to_end(&mut out)?;
Ok(out)
}
impl Storage {
pub fn new() -> Result<Self> {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let default_dir = home.join("tse-cache");
let tracker = home.join(".tse");
let datadir = if tracker.exists() {
let recorded = fs::read_to_string(&tracker).unwrap_or_default();
let p = PathBuf::from(recorded.trim());
if p.is_dir() { p } else { default_dir.clone() }
} else {
if !default_dir.exists() {
fs::create_dir_all(&default_dir)?;
}
fs::write(&tracker, default_dir.to_string_lossy().as_bytes())?;
default_dir.clone()
};
if !datadir.exists() {
fs::create_dir_all(&datadir)?;
}
Ok(Storage { datadir })
}
pub fn with_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
let datadir = dir.as_ref().to_path_buf();
fs::create_dir_all(&datadir)?;
Ok(Storage { datadir })
}
pub fn cache_dir(&self) -> &Path {
&self.datadir
}
fn resolve(&self, key: &str) -> PathBuf {
let key = key.strip_prefix("tse.").unwrap_or(key);
if let Some(rest) = key.strip_prefix("prices.") {
self.datadir.join("prices").join(format!("{rest}.csv"))
} else {
self.datadir.join(format!("{key}.csv"))
}
}
pub fn get_item(&self, key: &str) -> Result<String> {
let file = self.resolve(key);
if let Some(parent) = file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
if !file.exists() {
fs::write(&file, b"")?;
return Ok(String::new());
}
Ok(fs::read_to_string(&file)?)
}
pub fn set_item(&self, key: &str, value: &str) -> Result<()> {
let file = self.resolve(key);
if let Some(parent) = file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
fs::write(&file, value.as_bytes())?;
Ok(())
}
fn resolve_zipped(&self, key: &str, zip: bool) -> PathBuf {
let mut p = self.resolve(key);
if zip {
let name = format!("{}.gz", p.file_name().unwrap().to_string_lossy());
p.set_file_name(name);
}
p
}
pub fn get_item_async(&self, key: &str, zip: bool) -> Result<String> {
let file = self.resolve_zipped(key, zip);
if let Some(parent) = file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
if !file.exists() {
if !zip {
fs::write(&file, b"")?;
}
return Ok(String::new());
}
let raw = fs::read(&file)?;
if zip {
Ok(String::from_utf8_lossy(&gunzip(&raw)?).into_owned())
} else {
Ok(String::from_utf8_lossy(&raw).into_owned())
}
}
pub fn set_item_async(&self, key: &str, value: &str, zip: bool) -> Result<()> {
let file = self.resolve_zipped(key, zip);
if let Some(parent) = file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
if zip {
fs::write(&file, gzip(value.as_bytes())?)?;
} else {
fs::write(&file, value.as_bytes())?;
}
Ok(())
}
pub fn get_items(
&self,
selins: &HashSet<String>,
result: &mut HashMap<String, String>,
) -> Result<()> {
let dir = self.datadir.join("prices");
if !dir.exists() {
fs::create_dir_all(&dir)?;
return Ok(());
}
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().into_owned();
let key = name.strip_suffix(".csv").unwrap_or(&name).to_string();
if !selins.contains(&key) {
continue;
}
result.insert(key, fs::read_to_string(entry.path())?);
}
Ok(())
}
pub fn itd_get_items(
&self,
selins: &HashSet<String>,
full: bool,
) -> Result<HashMap<String, HashMap<String, ItdValue>>> {
let dir = self.datadir.join("intraday");
if !dir.exists() {
fs::create_dir_all(&dir)?;
return Ok(HashMap::new());
}
let mut result: HashMap<String, HashMap<String, ItdValue>> = HashMap::new();
for entry in fs::read_dir(&dir)? {
let entry = entry?;
if !entry.path().is_dir() {
continue;
}
let inscode = entry.file_name().to_string_lossy().into_owned();
if !selins.contains(&inscode) {
continue;
}
let mut files: HashMap<String, ItdValue> = HashMap::new();
for f in fs::read_dir(entry.path())? {
let f = f?;
let fname = f.file_name().to_string_lossy().into_owned();
let (deven, zipped) = match fname.strip_suffix(".gz") {
Some(stem) => (stem.to_string(), true),
None => (fname.clone(), false),
};
if full {
let raw = fs::read(f.path())?;
let val = if zipped {
ItdValue::Gzipped(raw)
} else {
ItdValue::Plain(String::from_utf8_lossy(&raw).into_owned())
};
files.insert(deven, val);
} else {
files.insert(deven, ItdValue::Present);
}
}
result.insert(inscode, files);
}
Ok(result)
}
pub fn itd_set_item(&self, key: &str, obj: &HashMap<String, ItdValue>) -> Result<()> {
let key = key.strip_prefix("tse.").unwrap_or(key);
let dir = self.datadir.join("intraday").join(key);
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
for (deven, val) in obj {
match val {
ItdValue::Plain(s) if s == "N/A" => {
fs::write(dir.join(deven), s.as_bytes())?;
}
ItdValue::Plain(s) => {
fs::write(dir.join(format!("{deven}.gz")), gzip(s.as_bytes())?)?;
}
ItdValue::Gzipped(bytes) => {
fs::write(dir.join(format!("{deven}.gz")), bytes)?;
}
ItdValue::Present => {}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum ItdValue {
Present,
Plain(String),
Gzipped(Vec<u8>),
}
impl ItdValue {
pub fn to_text(&self) -> Option<String> {
match self {
ItdValue::Plain(s) => Some(s.clone()),
ItdValue::Gzipped(b) => gunzip(b)
.ok()
.map(|v| String::from_utf8_lossy(&v).into_owned()),
ItdValue::Present => None,
}
}
}