Initial commit of mod_manager project

This commit is contained in:
vmko 2025-11-16 02:43:09 +03:00
parent a84b2e802d
commit 6ef66541b1
3 changed files with 837 additions and 0 deletions

459
Cargo.lock generated Normal file
View File

@ -0,0 +1,459 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "mod_manager"
version = "0.1.1"
dependencies = [
"env_logger",
"log",
"md5",
"rayon",
"serde",
"serde_json",
"sha1",
"sha2",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "mod_manager"
version = "0.1.1"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
sha1 = "0.10"
md5 = "0.7"
rayon = "1.10"
log = "0.4"
env_logger = "0.11"

364
src/main.rs Normal file
View File

@ -0,0 +1,364 @@
use log::{error, info, warn};
use md5;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
use std::collections::{HashMap, HashSet};
use std::fs::{self, File};
use std::io::{self, BufReader, BufWriter, Read};
use std::path::PathBuf;
const MODS_DIR: &str = "mods";
const JSON_FILE: &str = "mods-list.json";
const BUILD_CLIENT_DIR: &str = "build_client";
const BUILD_SERVER_DIR: &str = "build_server";
const CATEGORIES: [&str; 3] = ["client-only", "server-only", "universal"];
#[derive(Serialize, Deserialize, Clone)]
struct ModEntry {
name: String,
sha512: String,
sha256: String,
sha1: String,
md5: String,
}
#[derive(Serialize, Deserialize)]
struct ModList {
#[serde(rename = "client-only")]
client_only: Vec<ModEntry>,
#[serde(rename = "server-only")]
server_only: Vec<ModEntry>,
#[serde(rename = "universal")]
universal: Vec<ModEntry>,
}
#[derive(Clone)]
struct Hashes {
sha512: String,
sha256: String,
sha1: String,
md5: String,
}
fn main() {
env_logger::init();
let mods_dir = PathBuf::from(MODS_DIR);
let build_client_dir = PathBuf::from(BUILD_CLIENT_DIR);
let build_server_dir = PathBuf::from(BUILD_SERVER_DIR);
info!("Запуск менеджера модов");
println!("Выберите действие:");
println!("[1] Обновить моды");
println!("[2] Создать сборки");
println!("[3] Перевыбрать категории");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Ошибка ввода");
let choice = input.trim();
match choice {
"1" => update_mods(&mods_dir),
"2" => {
let data = load_json();
build(
&data.client_only,
&data.universal,
&mods_dir,
&build_client_dir,
"Client",
);
build(
&data.server_only,
&data.universal,
&mods_dir,
&build_server_dir,
"Server",
);
}
"3" => reassign_categories(&mods_dir),
_ => error!("Некорректный выбор"),
}
}
fn load_json() -> ModList {
match File::open(JSON_FILE) {
Ok(file) => serde_json::from_reader(BufReader::new(file)).unwrap_or_else(|e| {
warn!("Ошибка чтения JSON: {}. Создаём пустой.", e);
ModList {
client_only: vec![],
server_only: vec![],
universal: vec![],
}
}),
Err(_) => ModList {
client_only: vec![],
server_only: vec![],
universal: vec![],
},
}
}
fn save_json(data: &ModList) -> io::Result<()> {
let file = File::create(JSON_FILE)?;
serde_json::to_writer_pretty(BufWriter::new(file), data)?;
Ok(())
}
fn compute_hashes_for_file(path: &PathBuf) -> Hashes {
let file = File::open(path).expect("Не удалось открыть файл");
let mut reader = BufReader::new(file);
let mut sha512 = Sha512::new();
let mut sha256 = Sha256::new();
let mut sha1 = Sha1::new();
let mut md5_ctx = md5::Context::new();
let mut buffer = [0u8; 8192];
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
sha512.update(&buffer[..n]);
sha256.update(&buffer[..n]);
sha1.update(&buffer[..n]);
md5_ctx.consume(&buffer[..n]);
}
Err(e) => {
eprintln!("Ошибка чтения файла {}: {}", path.display(), e);
break;
}
}
}
Hashes {
sha512: format!("{:x}", sha512.finalize()),
sha256: format!("{:x}", sha256.finalize()),
sha1: format!("{:x}", sha1.finalize()),
md5: format!("{:x}", md5_ctx.compute()),
}
}
fn compute_hashes(paths: &[PathBuf]) -> Vec<(PathBuf, Hashes)> {
paths
.par_iter()
.map(|p| (p.clone(), compute_hashes_for_file(p)))
.collect()
}
fn ask_category(mod_name: &str) -> &'static str {
println!("\nМод: {}", mod_name);
println!("Выберите категорию:");
for (i, cat) in CATEGORIES.iter().enumerate() {
println!("[{}] {}", i + 1, cat);
}
loop {
let mut input = String::new();
if io::stdin().read_line(&mut input).is_ok() {
if let Ok(n) = input.trim().parse::<usize>() {
if n > 0 && n <= CATEGORIES.len() {
return CATEGORIES[n - 1];
}
}
}
println!("Некорректный ввод, попробуйте снова:");
}
}
fn get_jar_files(dir: &PathBuf) -> Vec<PathBuf> {
match fs::read_dir(dir) {
Ok(entries) => entries
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().map(|e| e == "jar").unwrap_or(false))
.collect(),
Err(e) => {
eprintln!("Не удалось прочитать папку {}: {}", MODS_DIR, e);
vec![]
}
}
}
fn process_new_mods(
paths: &[PathBuf],
existing_sha512: &HashSet<String>,
) -> Vec<(ModEntry, &'static str)> {
let mut new_entries = vec![];
let file_hashes = compute_hashes(paths);
for (path, hashes) in file_hashes {
if existing_sha512.contains(&hashes.sha512) {
continue;
}
let name = path
.file_name()
.expect("Нет имени файла")
.to_string_lossy()
.into_owned();
info!("Новый мод: {}", name);
let cat = ask_category(&name);
let entry = ModEntry {
name,
sha512: hashes.sha512,
sha256: hashes.sha256,
sha1: hashes.sha1,
md5: hashes.md5,
};
new_entries.push((entry, cat));
}
new_entries
}
fn build(
side_only: &[ModEntry],
universal: &[ModEntry],
mods_dir: &PathBuf,
build_dir: &PathBuf,
name: &str,
) {
if let Err(e) = fs::create_dir_all(build_dir) {
error!("Не удалось создать папку {}: {}", name, e);
return;
}
let mods: Vec<_> = side_only.iter().chain(universal).map(|e| &e.name).collect();
let count = mods.len();
for file_name in &mods {
let src = mods_dir.join(file_name);
let dst = build_dir.join(file_name);
if src.exists() {
if let Err(e) = fs::copy(&src, &dst) {
warn!("Не удалось скопировать {}: {}", file_name, e);
}
}
}
info!(
"{} сборка создана: {} модов в {}",
name,
count,
build_dir.display()
);
}
fn update_mods(mods_dir: &PathBuf) {
let mut data = load_json();
let existing_sha512: HashSet<String> = data
.client_only
.iter()
.chain(&data.server_only)
.chain(&data.universal)
.map(|e| e.sha512.clone())
.collect();
if !mods_dir.exists() {
error!("Папка {} не найдена!", MODS_DIR);
return;
}
let all_files = get_jar_files(mods_dir);
let current_names: HashSet<_> = all_files
.iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect();
for cat in [
&mut data.client_only,
&mut data.server_only,
&mut data.universal,
] {
cat.retain(|e| {
if !current_names.contains(&e.name) {
info!("Удалён устаревший мод: {}", e.name);
false
} else {
true
}
});
}
let new_entries = process_new_mods(&all_files, &existing_sha512);
for (entry, cat) in new_entries {
match cat {
"client-only" => data.client_only.push(entry),
"server-only" => data.server_only.push(entry),
"universal" => data.universal.push(entry),
_ => {}
}
}
if let Err(e) = save_json(&data) {
error!("Не удалось сохранить JSON: {}", e);
} else {
info!("Обновление завершено");
}
}
fn reassign_categories(mods_dir: &PathBuf) {
if !mods_dir.exists() {
error!("Папка {} не найдена!", MODS_DIR);
return;
}
let all_files = get_jar_files(mods_dir);
if PathBuf::from(JSON_FILE).exists() {
if let Err(e) = fs::remove_file(JSON_FILE) {
warn!("Не удалось удалить старый JSON: {}", e);
} else {
info!("Старый mods-list.json удалён");
}
}
let mut data = ModList {
client_only: vec![],
server_only: vec![],
universal: vec![],
};
let empty_sha512: HashSet<String> = HashSet::new();
// 1. Спрашиваем категории для всех модов
let mut temp_entries: Vec<(String, &'static str)> = vec![];
for path in &all_files {
let name = path
.file_name()
.expect("Нет имени файла")
.to_string_lossy()
.into_owned();
let cat = ask_category(&name);
temp_entries.push((name, cat));
}
// 2. Вычисляем хэши параллельно (в конце)
let file_hashes = compute_hashes(&all_files);
let mut hash_map: HashMap<String, Hashes> = HashMap::new();
for (path, hashes) in file_hashes {
let name = path
.file_name()
.expect("Нет имени файла")
.to_string_lossy()
.into_owned();
hash_map.insert(name, hashes);
}
// 3. Собираем ModEntry
for (name, cat) in temp_entries {
if let Some(hashes) = hash_map.get(&name) {
let entry = ModEntry {
name: name.clone(),
sha512: hashes.sha512.clone(),
sha256: hashes.sha256.clone(),
sha1: hashes.sha1.clone(),
md5: hashes.md5.clone(),
};
match cat {
"client-only" => data.client_only.push(entry),
"server-only" => data.server_only.push(entry),
"universal" => data.universal.push(entry),
_ => {}
}
}
}
if let Err(e) = save_json(&data) {
error!("Не удалось сохранить JSON: {}", e);
} else {
info!("Все моды перераспределены");
}
}