flowsurface_data/
lib.rs

1pub mod aggr;
2pub mod audio;
3pub mod chart;
4pub mod config;
5pub mod layout;
6pub mod log;
7pub mod panel;
8pub mod tickers_table;
9pub mod util;
10
11use std::fs::File;
12use std::io::{Read, Write};
13use std::path::PathBuf;
14
15pub use audio::AudioStream;
16pub use config::ScaleFactor;
17pub use config::sidebar::{self, Sidebar};
18pub use config::state::{Layouts, State};
19pub use config::theme::Theme;
20pub use config::timezone::UserTimezone;
21
22use ::log::{error, info, warn};
23pub use layout::{Dashboard, Layout, Pane};
24
25pub const SAVED_STATE_PATH: &str = "saved-state.json";
26
27#[derive(thiserror::Error, Debug, Clone)]
28pub enum InternalError {
29    #[error("Fetch error: {0}")]
30    Fetch(String),
31    #[error("Layout error: {0}")]
32    Layout(String),
33}
34
35pub fn write_json_to_file(json: &str, file_name: &str) -> std::io::Result<()> {
36    let path = data_path(Some(file_name));
37    let mut file = File::create(path)?;
38    file.write_all(json.as_bytes())?;
39    Ok(())
40}
41
42pub fn read_from_file(file_name: &str) -> Result<State, Box<dyn std::error::Error>> {
43    let path = data_path(Some(file_name));
44
45    let file_open_result = File::open(&path);
46    let mut file = match file_open_result {
47        Ok(file) => file,
48        Err(e) => return Err(Box::new(e)),
49    };
50
51    let mut contents = String::new();
52    if let Err(e) = file.read_to_string(&mut contents) {
53        return Err(Box::new(e));
54    }
55
56    match serde_json::from_str(&contents) {
57        Ok(state) => Ok(state),
58        Err(e) => {
59            // If parsing fails, backup the file
60            drop(file); // Close the file before renaming
61
62            // Create backup file with different name to prevent overwriting it
63            let backup_file_name = if let Some(pos) = file_name.rfind('.') {
64                format!("{}_old{}", &file_name[..pos], &file_name[pos..])
65            } else {
66                format!("{}_old", file_name)
67            };
68
69            let backup_path = data_path(Some(&backup_file_name));
70
71            if let Err(rename_err) = std::fs::rename(&path, &backup_path) {
72                warn!(
73                    "Failed to backup corrupted state file '{}' to '{}': {}",
74                    path.display(),
75                    backup_path.display(),
76                    rename_err
77                );
78            } else {
79                info!(
80                    "Backed up corrupted state file to '{}'. It can be restored manually.",
81                    backup_path.display()
82                );
83            }
84
85            Err(Box::new(e))
86        }
87    }
88}
89
90pub fn open_data_folder() -> Result<(), InternalError> {
91    let pathbuf = data_path(None);
92
93    if pathbuf.exists() {
94        if let Err(err) = open::that(&pathbuf) {
95            Err(InternalError::Layout(format!(
96                "Failed to open data folder: {:?}, error: {}",
97                pathbuf, err
98            )))
99        } else {
100            info!("Opened data folder: {:?}", pathbuf);
101            Ok(())
102        }
103    } else {
104        Err(InternalError::Layout(format!(
105            "Data folder does not exist: {:?}",
106            pathbuf
107        )))
108    }
109}
110
111pub fn data_path(path_name: Option<&str>) -> PathBuf {
112    if let Ok(path) = std::env::var("FLOWSURFACE_DATA_PATH") {
113        PathBuf::from(path)
114    } else {
115        let data_dir = dirs_next::data_dir().unwrap_or_else(|| PathBuf::from("."));
116        if let Some(path_name) = path_name {
117            data_dir.join("flowsurface").join(path_name)
118        } else {
119            data_dir.join("flowsurface")
120        }
121    }
122}
123
124fn cleanup_directory(data_path: &PathBuf) -> usize {
125    if !data_path.exists() {
126        warn!("Data path {:?} does not exist, skipping cleanup", data_path);
127        return 0;
128    }
129
130    let re =
131        regex::Regex::new(r".*-(\d{4}-\d{2}-\d{2})\.zip$").expect("Cleanup regex pattern is valid");
132    let today = chrono::Local::now().date_naive();
133    let mut deleted_files = Vec::new();
134
135    let entries = match std::fs::read_dir(data_path) {
136        Ok(entries) => entries,
137        Err(e) => {
138            error!("Failed to read data directory {:?}: {}", data_path, e);
139            return 0;
140        }
141    };
142
143    for entry in entries.filter_map(Result::ok) {
144        let symbol_dir = match std::fs::read_dir(entry.path()) {
145            Ok(dir) => dir,
146            Err(e) => {
147                error!("Failed to read symbol directory {:?}: {}", entry.path(), e);
148                continue;
149            }
150        };
151
152        for file in symbol_dir.filter_map(Result::ok) {
153            let path = file.path();
154            let Some(filename) = path.to_str() else {
155                continue;
156            };
157
158            if let Some(cap) = re.captures(filename)
159                && let Ok(file_date) = chrono::NaiveDate::parse_from_str(&cap[1], "%Y-%m-%d")
160            {
161                let days_old = today.signed_duration_since(file_date).num_days();
162                if days_old > 4 {
163                    if let Err(e) = std::fs::remove_file(&path) {
164                        error!("Failed to remove old file {}: {}", filename, e);
165                    } else {
166                        deleted_files.push(filename.to_string());
167                        info!("Removed old file: {}", filename);
168                    }
169                }
170            }
171        }
172    }
173
174    deleted_files.len()
175}
176
177pub fn cleanup_old_market_data() -> usize {
178    let paths = ["um", "cm"].map(|market_type| {
179        data_path(Some(&format!(
180            "market_data/binance/data/futures/{}/daily/aggTrades",
181            market_type
182        )))
183    });
184
185    let total_deleted: usize = paths.iter().map(cleanup_directory).sum();
186
187    info!("File cleanup completed. Deleted {} files", total_deleted);
188    total_deleted
189}