1use std::{
2 collections::BTreeMap,
3 path::{Path, PathBuf},
4 time::{SystemTime, UNIX_EPOCH},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{Result, SyncwebError};
10
11#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
13#[non_exhaustive]
14pub struct FolderStats {
15 pub upload: u64,
16 pub download: u64,
17 pub files_transferred: u64,
18}
19
20#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
22#[non_exhaustive]
23pub struct PeerStats {
24 pub upload: u64,
25 pub download: u64,
26 pub connection_count: u64,
27}
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[non_exhaustive]
32pub struct BandwidthStats {
33 pub total_upload: u64,
34 pub total_download: u64,
35 pub per_folder: BTreeMap<String, FolderStats>,
36 pub per_peer: BTreeMap<String, PeerStats>,
37 pub period_start: u64,
38}
39
40impl Default for BandwidthStats {
41 fn default() -> Self {
42 Self {
43 total_upload: 0,
44 total_download: 0,
45 per_folder: BTreeMap::new(),
46 per_peer: BTreeMap::new(),
47 period_start: now_seconds(),
48 }
49 }
50}
51
52impl BandwidthStats {
53 pub fn load(stats_path_impl: impl AsRef<Path>) -> Result<Self> {
59 let stats_path = stats_path_impl.as_ref();
60 if !stats_path.exists() {
61 return Ok(Self::default());
62 }
63 let bytes = std::fs::read(stats_path)
64 .map_err(|error| SyncwebError::operation("failed to read bandwidth stats", error))?;
65 serde_json::from_slice(&bytes)
66 .map_err(|error| SyncwebError::operation("failed to parse bandwidth stats", error))
67 }
68
69 pub fn save(&self, stats_path_impl: impl AsRef<Path>) -> Result<()> {
75 let stats_path = stats_path_impl.as_ref();
76 if let Some(parent) = stats_path.parent()
77 && !parent.as_os_str().is_empty()
78 {
79 std::fs::create_dir_all(parent)
80 .map_err(|error| SyncwebError::operation("failed to create stats directory", error))?;
81 }
82 let temporary = temporary_path(stats_path);
83 let result = (|| -> Result<()> {
84 let bytes = serde_json::to_vec_pretty(self)
85 .map_err(|error| SyncwebError::operation("failed to serialize bandwidth stats", error))?;
86 std::fs::write(&temporary, bytes)
87 .map_err(|error| SyncwebError::operation("failed to write temporary bandwidth stats", error))?;
88 std::fs::rename(&temporary, stats_path)
89 .map_err(|error| SyncwebError::operation("failed to persist bandwidth stats", error))
90 })();
91 if result.is_err()
92 && let Err(error) = std::fs::remove_file(&temporary)
93 {
94 tracing::warn!(
95 path = %temporary.display(),
96 ?error,
97 "failed to clean up temporary bandwidth stats"
98 );
99 }
100 result
101 }
102
103 pub fn reset(&mut self) {
105 *self = Self::default();
106 }
107
108 pub fn record_download(
110 &mut self,
111 bytes: u64,
112 files: u64,
113 folder_name_option: Option<&str>,
114 peer_id_option: Option<&str>,
115 ) {
116 self.total_download = self.total_download.saturating_add(bytes);
117 if let Some(folder_name) = folder_name_option {
118 let stats = self.per_folder.entry(folder_name.to_owned()).or_default();
119 stats.download = stats.download.saturating_add(bytes);
120 stats.files_transferred = stats.files_transferred.saturating_add(files);
121 }
122 if let Some(peer_id) = peer_id_option {
123 let stats = self.per_peer.entry(peer_id.to_owned()).or_default();
124 stats.download = stats.download.saturating_add(bytes);
125 }
126 }
127
128 pub fn record_upload(
130 &mut self,
131 bytes: u64,
132 files: u64,
133 folder_name_option: Option<&str>,
134 peer_id_option: Option<&str>,
135 ) {
136 self.total_upload = self.total_upload.saturating_add(bytes);
137 if let Some(folder_name) = folder_name_option {
138 let stats = self.per_folder.entry(folder_name.to_owned()).or_default();
139 stats.upload = stats.upload.saturating_add(bytes);
140 stats.files_transferred = stats.files_transferred.saturating_add(files);
141 }
142 if let Some(peer_id) = peer_id_option {
143 let stats = self.per_peer.entry(peer_id.to_owned()).or_default();
144 stats.upload = stats.upload.saturating_add(bytes);
145 }
146 }
147
148 pub fn record_connection(&mut self, peer: &str) {
150 let stats = self.per_peer.entry(peer.to_owned()).or_default();
151 stats.connection_count = stats.connection_count.saturating_add(1);
152 }
153}
154
155fn now_seconds() -> u64 {
156 SystemTime::now()
157 .duration_since(UNIX_EPOCH)
158 .map_or(0, |duration| duration.as_secs())
159}
160
161fn temporary_path(path: &Path) -> PathBuf {
162 path.with_extension(format!("tmp-{}", uuid::Uuid::new_v4()))
163}