steel_core/player/player_data_storage/
file_storage.rs1use super::known_players::{
2 KnownPlayersFile, decode_known_players_file, encode_known_players_file,
3};
4use super::permissions::{PlayerPermissionsFile, serialize_player_permissions_file};
5use super::stats::{PlayerStatsFile, serialize_player_stats_file};
6use super::{
7 GLOBAL_PLAYER_DATA_VERSION, GlobalPlayerData, GlobalPlayerDataFile, PlayerDataFile,
8 decode_global_file, decode_player_file, encode_global_file, encode_player_file,
9};
10use crate::permission::PermissionSubjectIndex;
11use crate::player::player_data::{PersistentPlayerData, PersistentStat};
12use crate::player::{KnownPlayers, Player};
13use rustc_hash::FxHashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use steel_utils::locks::{AsyncMutex, SyncMutex};
17use tokio::io::AsyncWriteExt;
18use tokio::sync::OwnedMutexGuard as OwnedAsyncMutexGuard;
19use tokio::{fs, io};
20use uuid::Uuid;
21
22pub(crate) struct FilePlayerDataStorage {
23 save_root: PathBuf,
24 file_locks: SyncMutex<FxHashMap<PathBuf, Arc<AsyncMutex<()>>>>,
25}
26
27impl FilePlayerDataStorage {
28 pub(crate) async fn new(save_root: PathBuf) -> io::Result<Self> {
29 fs::create_dir_all(save_root.join("global").join("players")).await?;
30 Ok(Self {
31 save_root,
32 file_locks: SyncMutex::new(FxHashMap::default()),
33 })
34 }
35
36 pub(crate) async fn save_domain(&self, domain: &str, player: &Player) -> io::Result<()> {
37 let uuid = player.gameprofile.id;
38 let data = PersistentPlayerData::from_player(player);
39 self.save_domain_data(domain, uuid, &data).await
40 }
41
42 pub(crate) async fn save_domain_data(
43 &self,
44 domain: &str,
45 uuid: Uuid,
46 data: &PersistentPlayerData,
47 ) -> io::Result<()> {
48 self.save_domain_player_data(domain, uuid, data).await?;
49 self.save_domain_player_stats(domain, uuid, data).await?;
50 Ok(())
51 }
52
53 pub(crate) async fn save_domain_player_data(
54 &self,
55 domain: &str,
56 uuid: Uuid,
57 data: &PersistentPlayerData,
58 ) -> io::Result<()> {
59 let file = PlayerDataFile::from_persistent(data)?;
60 let bytes = encode_player_file(&file)?;
61 self.write_atomic_player_data(&self.domain_players_dir(domain), uuid, &bytes)
62 .await?;
63 log::debug!("Saved player data for {uuid} in domain {domain}");
64
65 Ok(())
66 }
67
68 pub(crate) async fn save_domain_player_stats(
69 &self,
70 domain: &str,
71 uuid: Uuid,
72 data: &PersistentPlayerData,
73 ) -> io::Result<()> {
74 let player_stats_file = PlayerStatsFile::from_persistent_stats(&data.stats)?;
75 let toml_string = serialize_player_stats_file(&player_stats_file);
76 let final_path = Self::player_stats_file(&self.domain_players_dir(domain), uuid);
77 let _guard = self.file_lock(&final_path).await;
78 Self::write_atomic_path_locked(&final_path, toml_string.as_bytes()).await?;
79 log::debug!("Saved player stats for {uuid} in domain {domain}");
80
81 Ok(())
82 }
83
84 pub(crate) async fn load_domain(
85 &self,
86 domain: &str,
87 uuid: Uuid,
88 ) -> io::Result<Option<PersistentPlayerData>> {
89 let Some(mut data) = self.load_domain_player_data(domain, uuid).await? else {
90 return Ok(None);
91 };
92
93 data.stats = match self.load_domain_player_stats(domain, uuid).await {
94 Ok(Some(stats)) => stats,
95 Ok(None) => {
96 log::debug!("Using empty stats for {uuid} in domain {domain}");
97 Vec::new()
98 }
99 Err(e) => {
100 log::error!("Could not load stats for {uuid} in domain {domain}: {e}");
101 Vec::new()
102 }
103 };
104
105 Ok(Some(data))
106 }
107
108 pub(crate) async fn load_domain_player_data(
109 &self,
110 domain: &str,
111 uuid: Uuid,
112 ) -> io::Result<Option<PersistentPlayerData>> {
113 let domain_dir = self.domain_players_dir(domain);
114 let path = Self::player_data_file(&domain_dir, uuid);
115 let _guard = self.file_lock(&path).await;
116 if !Self::recover_missing_atomic_path_locked(&path).await? {
117 return Ok(None);
118 }
119 let bytes = fs::read(&path).await?;
120 let file = decode_player_file(&bytes)?;
121 let data = file.into_persistent()?;
122 log::debug!("Loaded player data for {uuid} in domain {domain}");
123
124 Ok(Some(data))
125 }
126
127 pub(crate) async fn load_domain_player_stats(
128 &self,
129 domain: &str,
130 uuid: Uuid,
131 ) -> io::Result<Option<Vec<PersistentStat>>> {
132 let domain_dir = self.domain_players_dir(domain);
133 let path = Self::player_stats_file(&domain_dir, uuid);
134 let _guard = self.file_lock(&path).await;
135 if !Self::recover_missing_atomic_path_locked(&path).await? {
136 return Ok(None);
137 }
138
139 let string = fs::read_to_string(&path).await?;
140 let stats_file: PlayerStatsFile = toml::from_str(&string).map_err(|e| {
141 io::Error::new(
142 io::ErrorKind::InvalidData,
143 format!("Invalid player stats for {uuid} in domain {domain}: {e}"),
144 )
145 })?;
146 let persistent_stats = stats_file.into_persistent_stats();
147 log::debug!("Loaded player stats for {uuid} in domain {domain}");
148
149 Ok(Some(persistent_stats))
150 }
151
152 pub(crate) async fn load_global(&self, uuid: Uuid) -> io::Result<Option<GlobalPlayerData>> {
153 let path = Self::player_data_file(&self.global_players_dir(), uuid);
154 let _guard = self.file_lock(&path).await;
155 if !Self::recover_missing_atomic_path_locked(&path).await? {
156 return Ok(None);
157 }
158 let bytes = fs::read(&path).await?;
159 let file = decode_global_file(&bytes)?;
160 Ok(Some(GlobalPlayerData {
161 last_active_domain: file.last_active_domain,
162 }))
163 }
164
165 pub(crate) async fn load_permission_subjects(&self) -> io::Result<PermissionSubjectIndex> {
166 self.load_player_permissions_file()
167 .await?
168 .into_subject_index()
169 }
170
171 pub(crate) async fn load_known_players(&self) -> io::Result<KnownPlayers> {
172 let path = self.known_players_file();
173 let _guard = self.file_lock(&path).await;
174 match Self::read_known_players_file_locked(&path).await {
175 Ok(players) => Ok(players),
176 Err(error) => {
177 log::warn!(
178 "Failed to load known player cache from {}: {error}. Starting with an empty cache",
179 path.display()
180 );
181 Ok(KnownPlayers::new())
182 }
183 }
184 }
185
186 async fn read_known_players_file_locked(path: &Path) -> io::Result<KnownPlayers> {
187 if !Self::recover_missing_atomic_path_locked(path).await? {
188 return Ok(KnownPlayers::new());
189 }
190 let bytes = fs::read(path).await?;
191 decode_known_players_file(&bytes)?.into_known_players()
192 }
193
194 pub(crate) async fn save_known_players_if_current(
195 &self,
196 players: &KnownPlayers,
197 is_current: impl FnOnce() -> bool + Send,
198 ) -> io::Result<bool> {
199 let path = self.known_players_file();
200 let _guard = self.file_lock(&path).await;
201 if !is_current() {
202 return Ok(false);
203 }
204 let bytes = encode_known_players_file(&KnownPlayersFile::from_known_players(players))?;
205 Self::write_atomic_path_locked(&path, &bytes).await?;
206 Ok(true)
207 }
208
209 pub(crate) async fn save_global(&self, uuid: Uuid, data: &GlobalPlayerData) -> io::Result<()> {
210 let file = GlobalPlayerDataFile {
211 data_version: GLOBAL_PLAYER_DATA_VERSION,
212 last_active_domain: data.last_active_domain.clone(),
213 };
214 let bytes = encode_global_file(&file)?;
215 self.write_atomic_player_data(&self.global_players_dir(), uuid, &bytes)
216 .await
217 }
218
219 pub(crate) async fn save_permission_subjects(
220 &self,
221 subjects: &PermissionSubjectIndex,
222 ) -> io::Result<()> {
223 let path = self.player_permissions_file();
224 let _guard = self.file_lock(&path).await;
225 let file = PlayerPermissionsFile::from_subject_index(subjects);
226 self.write_player_permissions_file_locked(&path, &file)
227 .await
228 }
229
230 pub(crate) async fn load_player_permissions_file(&self) -> io::Result<PlayerPermissionsFile> {
231 let path = self.player_permissions_file();
232 let _guard = self.file_lock(&path).await;
233 self.read_player_permissions_file_locked(&path).await
234 }
235
236 pub(crate) async fn read_player_permissions_file_locked(
237 &self,
238 path: &Path,
239 ) -> io::Result<PlayerPermissionsFile> {
240 if !Self::recover_missing_atomic_path_locked(path).await? {
241 return Ok(PlayerPermissionsFile::default());
242 }
243 let contents = fs::read_to_string(path).await?;
244 let file = toml::from_str::<PlayerPermissionsFile>(&contents).map_err(|error| {
245 io::Error::new(
246 io::ErrorKind::InvalidData,
247 format!(
248 "invalid player permissions TOML in {}: {error}",
249 path.display()
250 ),
251 )
252 })?;
253 file.validate()?;
254 Ok(file)
255 }
256
257 pub(crate) async fn write_player_permissions_file_locked(
258 &self,
259 path: &Path,
260 file: &PlayerPermissionsFile,
261 ) -> io::Result<()> {
262 let contents = serialize_player_permissions_file(file).map_err(|error| {
263 io::Error::new(
264 io::ErrorKind::InvalidData,
265 format!("failed to serialize player permissions TOML: {error}"),
266 )
267 })?;
268 Self::write_atomic_path_locked(path, contents.as_bytes()).await
269 }
270
271 pub(crate) fn global_dir(&self) -> PathBuf {
272 self.save_root.join("global")
273 }
274
275 pub(crate) fn global_players_dir(&self) -> PathBuf {
276 self.global_dir().join("players")
277 }
278
279 pub(crate) fn player_permissions_file(&self) -> PathBuf {
280 self.global_dir().join("player_permissions.toml")
281 }
282
283 pub(crate) fn known_players_file(&self) -> PathBuf {
284 self.global_dir().join("known_players.dat")
285 }
286
287 pub(crate) fn domain_players_dir(&self, domain: &str) -> PathBuf {
288 self.save_root.join(domain).join("players")
289 }
290
291 pub(crate) fn player_data_file(players_dir: &Path, uuid: Uuid) -> PathBuf {
292 players_dir.join(format!("data/{uuid}"))
293 }
294
295 pub(crate) fn player_stats_file(players_dir: &Path, uuid: Uuid) -> PathBuf {
296 players_dir.join(format!("stats/{uuid}.toml"))
297 }
298
299 async fn file_lock(&self, path: &Path) -> OwnedAsyncMutexGuard<()> {
300 let mutex = self
301 .file_locks
302 .lock()
303 .entry(path.to_path_buf())
304 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
305 .clone();
306
307 mutex.lock_owned().await
308 }
309
310 async fn write_atomic_player_data(
311 &self,
312 players_dir: &Path,
313 uuid: Uuid,
314 bytes: &[u8],
315 ) -> io::Result<()> {
316 let final_path = Self::player_data_file(players_dir, uuid);
317 let _guard = self.file_lock(&final_path).await;
318 Self::write_atomic_path_locked(&final_path, bytes).await
319 }
320
321 pub(crate) async fn write_atomic_path_locked(
322 final_path: &Path,
323 bytes: &[u8],
324 ) -> io::Result<()> {
325 let Some(parent) = final_path.parent() else {
326 return Err(io::Error::new(
327 io::ErrorKind::InvalidInput,
328 "atomic write path has no parent",
329 ));
330 };
331 fs::create_dir_all(parent).await?;
332 let temp_path = Self::atomic_temp_path(final_path);
333 let backup_path = Self::atomic_backup_path(final_path);
334 let backup_temp_path = Self::atomic_temp_path(&backup_path);
335
336 Self::write_synced_file(&temp_path, bytes).await?;
337 if fs::try_exists(final_path).await? {
338 Self::copy_synced_file(final_path, &backup_temp_path).await?;
339 fs::rename(&backup_temp_path, &backup_path).await?;
340 }
341 fs::rename(&temp_path, final_path).await?;
342 if let Err(error) = Self::sync_parent(parent).await {
343 tracing::error!(
344 %error,
345 path = %final_path.display(),
346 "Atomic data-file replacement committed, but directory sync failed; crash durability is uncertain"
347 );
348 }
349 Ok(())
350 }
351
352 pub(crate) fn atomic_temp_path(path: &Path) -> PathBuf {
353 let extension = path.extension().and_then(|value| value.to_str());
354 path.with_extension(match extension {
355 Some(extension) => format!("{extension}.tmp"),
356 None => "tmp".to_owned(),
357 })
358 }
359
360 pub(crate) fn atomic_backup_path(path: &Path) -> PathBuf {
361 let extension = path.extension().and_then(|value| value.to_str());
362 path.with_extension(match extension {
363 Some(extension) => format!("{extension}_old"),
364 None => "old".to_owned(),
365 })
366 }
367
368 async fn recover_missing_atomic_path_locked(final_path: &Path) -> io::Result<bool> {
369 if fs::try_exists(final_path).await? {
370 return Ok(true);
371 }
372
373 let backup_path = Self::atomic_backup_path(final_path);
374 if fs::try_exists(&backup_path).await? {
375 fs::rename(&backup_path, final_path).await?;
376 let Some(parent) = final_path.parent() else {
377 return Err(io::Error::new(
378 io::ErrorKind::InvalidInput,
379 "atomic recovery path has no parent",
380 ));
381 };
382 Self::sync_parent(parent).await?;
383 let temp_path = Self::atomic_temp_path(final_path);
384 if fs::try_exists(&temp_path).await?
385 && let Err(error) = fs::remove_file(&temp_path).await
386 {
387 tracing::warn!(
388 %error,
389 path = %temp_path.display(),
390 "Failed to remove an uncommitted atomic-write temporary file"
391 );
392 }
393 tracing::warn!(
394 path = %final_path.display(),
395 backup = %backup_path.display(),
396 "Recovered a missing data file from its last committed backup"
397 );
398 return Ok(true);
399 }
400
401 let temp_path = Self::atomic_temp_path(final_path);
402 if fs::try_exists(&temp_path).await? {
403 if let Err(error) = fs::remove_file(&temp_path).await {
404 tracing::warn!(
405 %error,
406 path = %temp_path.display(),
407 "Failed to remove an uncommitted atomic-write temporary file"
408 );
409 }
410 tracing::warn!(
411 path = %final_path.display(),
412 temporary = %temp_path.display(),
413 "Discarded an interrupted data-file publication with no committed generation"
414 );
415 }
416
417 Ok(false)
418 }
419
420 async fn write_synced_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
421 let mut file = fs::File::create(path).await?;
422 file.write_all(bytes).await?;
423 file.sync_all().await
424 }
425
426 async fn copy_synced_file(source: &Path, destination: &Path) -> io::Result<()> {
427 let mut source = fs::File::open(source).await?;
428 let mut destination = fs::File::create(destination).await?;
429 io::copy(&mut source, &mut destination).await?;
430 destination.sync_all().await
431 }
432
433 async fn sync_parent(parent: &Path) -> io::Result<()> {
434 if cfg!(unix) {
437 fs::File::open(parent).await?.sync_all().await?;
438 }
439 Ok(())
440 }
441}