Skip to main content

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