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