Cactus/fs_manager/
mod.rs

1use std::fs::{self, remove_dir_all, File, OpenOptions};
2use std::io::{self, BufRead, Seek, SeekFrom};
3use std::path::Path;
4pub(crate) mod utils;
5use crate::config::{Difficulty, Gamemode};
6use crate::{config, consts, gracefully_exit};
7use cactus_world::level::{create_nbt, LevelDat, VersionInfo};
8use colored::Colorize;
9use log::{error, info, warn};
10use serde::{Deserialize, Serialize};
11use std::io::Read;
12use std::io::Write;
13
14// Initializes the server's required files and directories
15pub fn init() -> std::io::Result<()> {
16    eula()?;
17    create_server_properties()
18}
19
20/// Checks if the eula is agreed, if not shutdown the server with failure code.
21fn eula() -> io::Result<()> {
22    let path = Path::new(consts::file_paths::EULA);
23    if !path.exists() {
24        create_eula()?;
25        let content = "Please agree to the 'eula.txt' and start the server again.";
26        warn!("{}", content.bright_red().bold());
27        gracefully_exit(crate::ExitCode::Failure);
28    } else {
29        let is_agreed_eula = check_eula()?;
30        if !is_agreed_eula {
31            let error_content = "Cannot start the server, please agree to the 'eula.txt'";
32            error!("{}", error_content.bright_red().bold().blink());
33            gracefully_exit(crate::ExitCode::Failure);
34        }
35        Ok(())
36    }
37}
38
39/// Creates the 'server.properties' file if it does not already exist.
40fn create_server_properties() -> io::Result<()> {
41    let path = Path::new(consts::file_paths::PROPERTIES);
42    let content = consts::file_contents::server_properties();
43
44    utils::create_file(path, Some(&content))
45}
46
47/// Creates the 'eula.txt' file if it does not already exist.
48fn create_eula() -> io::Result<()> {
49    let path = Path::new(consts::file_paths::EULA);
50    let content = consts::file_contents::eula();
51
52    utils::create_file(path, Some(&content))
53}
54
55/// Check if the 'eula.txt' has been agreed to.
56fn check_eula() -> io::Result<bool> {
57    let file = File::open(Path::new(consts::file_paths::EULA))?;
58    let reader = io::BufReader::new(file);
59
60    for line in reader.lines() {
61        let line = line?;
62        if line.starts_with("eula=") {
63            let eula_value = line.split('=').nth(1).unwrap_or("").to_lowercase();
64            return Ok(eula_value == "true");
65        }
66    }
67
68    Ok(false)
69}
70
71pub fn create_other_files() {
72    match utils::create_file(Path::new(consts::file_paths::BANNED_IP), None) {
73        Ok(_) => (),
74        Err(e) => error!(
75            "Failed to create the file {} as error:{}",
76            consts::file_paths::BANNED_IP,
77            e
78        ),
79    }
80    match utils::create_file(Path::new(consts::file_paths::BANNED_PLAYERS), None) {
81        Ok(_) => (),
82        Err(e) => error!(
83            "Failed to create the file {} as error:{}",
84            consts::file_paths::BANNED_PLAYERS,
85            e
86        ),
87    }
88    match utils::create_file(Path::new(consts::file_paths::OPERATORS), None) {
89        Ok(_) => (),
90        Err(e) => error!(
91            "Failed to create the file {} as error:{}",
92            consts::file_paths::OPERATORS,
93            e
94        ),
95    }
96    match utils::create_file(Path::new(consts::file_paths::SESSION), None) {
97        Ok(_) => (),
98        Err(e) => error!(
99            "Failed to create the file {} as error:{}",
100            consts::file_paths::SESSION,
101            e
102        ),
103    }
104    match utils::create_file(Path::new(consts::file_paths::USERCACHE), None) {
105        Ok(_) => (),
106        Err(e) => error!(
107            "Failed to create the file {} as error:{}",
108            consts::file_paths::USERCACHE,
109            e
110        ),
111    }
112    match utils::create_file(Path::new(consts::file_paths::WHITELIST), None) {
113        Ok(_) => (),
114        Err(e) => error!(
115            "Failed to create the file {} as error:{}",
116            consts::file_paths::WHITELIST,
117            e
118        ),
119    }
120    match create_level_file() {
121        Ok(_) => (),
122        Err(e) => error!(
123            "Failed to create the file at {} as error {}",
124            consts::file_paths::LEVEL,
125            e
126        ),
127    }
128}
129pub fn create_dirs() {
130    match utils::create_dir(Path::new(consts::directory_paths::LOGS)) {
131        Ok(_) => (),
132        Err(e) => info!(
133            "Failed to create dir{} as error: {}",
134            consts::directory_paths::LOGS,
135            e
136        ),
137    }
138
139    match utils::create_dir(Path::new(consts::directory_paths::WORLDS_DIRECTORY)) {
140        Ok(_) => (),
141        Err(e) => info!(
142            "Failed to create dir{} as error: {}",
143            consts::directory_paths::WORLDS_DIRECTORY,
144            e
145        ),
146    }
147
148    match utils::create_dir(Path::new(consts::directory_paths::OVERWORLD)) {
149        Ok(_) => (),
150        Err(e) => info!(
151            "Failed to create dir{} as error: {}",
152            consts::directory_paths::OVERWORLD,
153            e
154        ),
155    }
156
157    match utils::create_dir(Path::new(consts::directory_paths::THE_END)) {
158        Ok(_) => (),
159        Err(e) => info!(
160            "Failed to create dir{} as error: {}",
161            consts::directory_paths::THE_END,
162            e
163        ),
164    }
165
166    match utils::create_dir(Path::new(consts::directory_paths::NETHER)) {
167        Ok(_) => (),
168        Err(e) => info!(
169            "Failed to create dir{} as error: {}",
170            consts::directory_paths::NETHER,
171            e
172        ),
173    }
174}
175#[derive(Serialize, Deserialize)]
176struct Player {
177    uuid: String,
178    name: String,
179    level: u8,
180    bypassesPlayerLimit: bool,
181}
182
183pub fn write_ops_json(
184    filename: &str,
185    uuid: &str,
186    name: &str,
187    level: u8,
188    bypasses_player_limit: bool,
189) -> std::io::Result<()> {
190    let mut file = OpenOptions::new()
191        .read(true)
192        .write(true)
193        .create(true)
194        .open(filename)?;
195
196    let mut content = String::new();
197    file.read_to_string(&mut content)?;
198
199    if content.starts_with('\u{feff}') {
200        content = content.trim_start_matches('\u{feff}').to_string();
201    }
202
203    let mut players: Vec<Player> = if content.trim().is_empty() {
204        Vec::new()
205    } else {
206        serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())
207    };
208
209    if !players.iter().any(|p| p.uuid == uuid) {
210        players.push(Player {
211            uuid: uuid.to_string(),
212            name: name.to_string(),
213            level,
214            bypassesPlayerLimit: bypasses_player_limit,
215        });
216        info!("Made {} a server operator", name.to_string())
217    } else {
218        warn!("Nothing changed. The player already is an operator")
219    }
220
221    // Réécrire le fichier avec le contenu mis à jour
222    file.set_len(0)?;
223    file.seek(SeekFrom::Start(0))?;
224    file.write_all(serde_json::to_string_pretty(&players)?.as_bytes())?;
225
226    Ok(())
227}
228/// Removes all files related to the server, excluding the server.
229///
230/// I am not sure if this is a good idea, because it takes some time to maintain and is not very
231/// useful but it's mostly for dev purpose.
232pub fn clean_files() -> Result<(), std::io::Error> {
233    // Define a helper function to handle file removals
234    fn remove_file(file_path: &str) -> Result<(), std::io::Error> {
235        match fs::remove_file(file_path) {
236            Ok(_) => {
237                info!("File deleted: {}", file_path);
238                Ok(())
239            }
240            Err(e) => {
241                info!("Error when deleting file {}: {}", file_path, e);
242                Err(e)
243            }
244        }
245    }
246
247    // Define a helper function to handle directory removals
248    fn remove_dir(dir_path: &str) -> Result<(), std::io::Error> {
249        match fs::remove_dir(dir_path) {
250            Ok(_) => {
251                info!("Directory deleted: {}", dir_path);
252                Ok(())
253            }
254            Err(e) => {
255                info!("Error when deleting directory {}: {}", dir_path, e);
256                Err(e)
257            }
258        }
259    }
260
261    // List all files to be deleted
262    let files = [
263        consts::file_paths::EULA,
264        consts::file_paths::PROPERTIES,
265        consts::file_paths::BANNED_IP,
266        consts::file_paths::BANNED_PLAYERS,
267        consts::file_paths::OPERATORS,
268        consts::file_paths::SESSION,
269        consts::file_paths::USERCACHE,
270        consts::file_paths::WHITELIST,
271        consts::file_paths::LEVEL,
272    ];
273
274    // Delete files using the `remove_file` helper function
275    for file in &files {
276        remove_file(file)?;
277    }
278
279    // List all directories to be deleted
280    let directories = [
281        consts::directory_paths::LOGS,
282        consts::directory_paths::NETHER,
283        consts::directory_paths::OVERWORLD,
284        consts::directory_paths::THE_END,
285        consts::directory_paths::WORLDS_DIRECTORY,
286    ];
287
288    // Delete directories using the `remove_dir` helper function
289    for dir in &directories {
290        remove_dir_all(dir)?;
291    }
292    let info = "[Info]".green();
293    println!(
294        "{} Files cleaned successfully before starting the server.",
295        info
296    );
297    gracefully_exit(crate::ExitCode::Success);
298}
299fn create_level_file() -> Result<(), std::io::Error> {
300    if Path::new(consts::file_paths::LEVEL).exists() {
301        info!("level.dat file already exist, not altering it");
302        return Ok(());
303    }
304    let server_version = VersionInfo {
305        id: 4440,
306        snapshot: false,
307        series: "main".into(),
308        name: consts::minecraft::VERSION.into(),
309    };
310    let data = LevelDat {
311        version: server_version,
312        difficulty: match config::Settings::new().difficulty {
313            Difficulty::Easy => 0,
314            Difficulty::Normal => 1,
315            Difficulty::Hard => 2,
316        },
317        game_type: match config::Settings::new().gamemode {
318            Gamemode::Survival => 0,
319            Gamemode::Creative => 1,
320            Gamemode::Spectator => 3,
321            Gamemode::Adventure => 2,
322        },
323
324        hardcore: config::Settings::new().hardcore,
325        level_name: match config::Settings::new().level_name {
326            Some(words) => words,
327            _ => "Overworld".into(),
328        },
329
330        ..Default::default()
331    };
332
333    let result = create_nbt(&data, consts::file_paths::LEVEL);
334    if result.is_ok() {
335        info!("File created at {}", consts::file_paths::LEVEL)
336    }
337    result
338}