mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-10-31 04:17:44 +00:00
fix(docker): make data persistence reliable (#3692)
* fix(docker): make data persistence reliable * fix(docker): preserve existing bind-mounted data
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
fs::{self, File},
|
||||
io::ErrorKind,
|
||||
path::Path,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
@@ -362,7 +362,14 @@ impl WorldInfoReader for AnvilLevelInfo {
|
||||
fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError> {
|
||||
let path = level_folder.join(LEVEL_DAT_FILE_NAME);
|
||||
|
||||
let root = read_gzip_compound_tag(File::open(path)?)
|
||||
let file = match File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(error) if error.kind() == ErrorKind::NotFound => {
|
||||
return Err(WorldInfoError::InfoNotFound);
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let root = read_gzip_compound_tag(file)
|
||||
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
|
||||
let Some(data) = root.get_compound(LEVEL_DATA_TAG) else {
|
||||
error!("The level.dat file has no {LEVEL_DATA_TAG} compound and is therefore corrupt");
|
||||
@@ -419,6 +426,8 @@ impl WorldInfoWriter for AnvilLevelInfo {
|
||||
info: &LevelData,
|
||||
level_folder: &Path,
|
||||
) -> Result<(), WorldInfoError> {
|
||||
fs::create_dir_all(level_folder)?;
|
||||
|
||||
let start = SystemTime::now();
|
||||
let since_the_epoch = start.duration_since(UNIX_EPOCH).unwrap_or_default();
|
||||
let mut level_data = info.clone();
|
||||
@@ -442,9 +451,9 @@ impl WorldInfoWriter for AnvilLevelInfo {
|
||||
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))?;
|
||||
|
||||
if path.exists() {
|
||||
let _ = std::fs::copy(&path, &path_old);
|
||||
fs::copy(&path, &path_old)?;
|
||||
}
|
||||
let _ = std::fs::rename(&path_new, &path);
|
||||
fs::rename(&path_new, &path)?;
|
||||
|
||||
let data_version = level_data.data_version;
|
||||
|
||||
@@ -540,6 +549,7 @@ mod test {
|
||||
use pumpkin_util::{Difficulty, world_seed::Seed};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::ErrorKind,
|
||||
path::Path,
|
||||
sync::LazyLock,
|
||||
};
|
||||
@@ -620,6 +630,44 @@ mod test {
|
||||
assert_eq!(data.world_gen_settings.seed, seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_missing_world_directory_when_writing() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let level_folder = temp_dir.path().join("world");
|
||||
|
||||
AnvilLevelInfo
|
||||
.write_world_info(&LevelData::default(Seed(42)), &level_folder)
|
||||
.unwrap();
|
||||
|
||||
assert!(level_folder.join(LEVEL_DAT_FILE_NAME).is_file());
|
||||
assert!(!level_folder.join("level.dat_new").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_missing_level_dat_as_info_not_found() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let error = AnvilLevelInfo.read_world_info(temp_dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, WorldInfoError::InfoNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_writer_io_errors_as_io_errors() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let level_folder = temp_dir.path().join("world");
|
||||
File::create(&level_folder).unwrap();
|
||||
|
||||
let error = AnvilLevelInfo
|
||||
.write_world_info(&LevelData::default(Seed(42)), &level_folder)
|
||||
.unwrap_err();
|
||||
|
||||
let WorldInfoError::IoError(error) = error else {
|
||||
panic!("expected writer I/O error");
|
||||
};
|
||||
assert_eq!(error.kind(), ErrorKind::AlreadyExists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_level_dat_without_series() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
@@ -588,8 +588,8 @@ impl LevelData {
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WorldInfoError {
|
||||
#[error("Io error: {0}")]
|
||||
IoError(std::io::ErrorKind),
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Info not found!")]
|
||||
InfoNotFound,
|
||||
#[error("Deserialization error: {0}")]
|
||||
@@ -606,15 +606,6 @@ pub enum WorldInfoError {
|
||||
UnsupportedLevelVersion(i32),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for WorldInfoError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
match value.kind() {
|
||||
std::io::ErrorKind::NotFound => Self::InfoNotFound,
|
||||
value => Self::IoError(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -27,6 +27,7 @@ use plugin::server::server_load::{LoadType, ServerLoadEvent};
|
||||
use pumpkin_config::{AdvancedConfiguration, BasicConfiguration, TelemetryConfig};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use pumpkin_util::text::color::{Color, NamedColor};
|
||||
use pumpkin_world::world_info::WorldInfoError;
|
||||
use rustyline::Editor;
|
||||
use rustyline::history::FileHistory;
|
||||
use rustyline::{Config, error::ReadlineError};
|
||||
@@ -251,14 +252,14 @@ impl PumpkinServer {
|
||||
advanced_config: AdvancedConfiguration,
|
||||
telemetry_config: TelemetryConfig,
|
||||
vanilla_data: VanillaData,
|
||||
) -> Self {
|
||||
) -> Result<Self, WorldInfoError> {
|
||||
let server = Server::new(
|
||||
basic_config,
|
||||
advanced_config,
|
||||
telemetry_config,
|
||||
vanilla_data,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
adjust_file_descriptor_limit();
|
||||
@@ -349,12 +350,12 @@ impl PumpkinServer {
|
||||
};
|
||||
let nethernet_listener = Self::bind_nethernet(&server, ice_socket).await;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
server,
|
||||
tcp_listener,
|
||||
bedrock_status,
|
||||
nethernet_listener,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn bind_nethernet(
|
||||
|
||||
@@ -126,7 +126,11 @@ async fn main() {
|
||||
config.telemetry,
|
||||
vanilla_data,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::error!("Failed to initialize world storage: {error}");
|
||||
exit(1);
|
||||
});
|
||||
let plugin_wait_time = pumpkin_server.init_plugins().await;
|
||||
|
||||
let time_elapsed = time.elapsed().saturating_sub(plugin_wait_time);
|
||||
|
||||
@@ -156,13 +156,12 @@ pub struct Server {
|
||||
|
||||
impl Server {
|
||||
#[expect(clippy::too_many_lines)]
|
||||
#[must_use]
|
||||
pub async fn new(
|
||||
basic_config: BasicConfiguration,
|
||||
advanced_config: AdvancedConfiguration,
|
||||
telemetry_config: TelemetryConfig,
|
||||
vanilla_data: VanillaData,
|
||||
) -> Arc<Self> {
|
||||
) -> Result<Arc<Self>, WorldInfoError> {
|
||||
let permission_manager = Arc::new(PermissionManager::new());
|
||||
// First register the default commands. After that, plugins can put in their own.
|
||||
let command_dispatcher = ArcSwap::from_pointee(default_dispatcher(
|
||||
@@ -201,9 +200,7 @@ impl Server {
|
||||
);
|
||||
let default_data =
|
||||
LevelData::from_world_generator(basic_config.seed, &overworld_gen);
|
||||
if let Err(err) = AnvilLevelInfo.write_world_info(&default_data, &world_path) {
|
||||
error!("Failed to save level.dat: {err}");
|
||||
}
|
||||
AnvilLevelInfo.write_world_info(&default_data, &world_path)?;
|
||||
default_data
|
||||
}
|
||||
Err(
|
||||
@@ -436,7 +433,7 @@ impl Server {
|
||||
.datapack_manager
|
||||
.execute_function(&server, &source, "#minecraft:load");
|
||||
|
||||
server
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
/// Spawns a task associated with this server. All tasks spawned with this method are awaited
|
||||
|
||||
@@ -8,9 +8,17 @@ services:
|
||||
- "25565:25565"
|
||||
- "19132:19132/udp"
|
||||
volumes:
|
||||
# Pumpkin runs as UID/GID 2613. On Linux hosts, make sure the data
|
||||
# directory has the correct ownership before starting the server:
|
||||
# mkdir -p ./data
|
||||
# sudo chown -R 2613:2613 ./data
|
||||
- ./data:/pumpkin
|
||||
# Use the following, if you want to make your config immutable
|
||||
#- ./config:/pumpkin/config:ro
|
||||
# As an alternative, Docker can manage the storage volume. Existing
|
||||
# installations must migrate ./data before switching to avoid creating
|
||||
# a new, empty world:
|
||||
# - pumpkin-data:/pumpkin
|
||||
# Use the following mount if you want to make your config immutable.
|
||||
# - ./pumpkin.toml:/pumpkin/pumpkin.toml:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
@@ -19,3 +27,7 @@ services:
|
||||
cap_drop:
|
||||
- ALL
|
||||
read_only: true
|
||||
|
||||
# Uncomment this section when using the pumpkin-data volume above.
|
||||
# volumes:
|
||||
# pumpkin-data:
|
||||
|
||||
Reference in New Issue
Block a user