use crate::error::McDataError;
use once_cell::sync::{Lazy, OnceCell};
use std::fs::{self, File};
use std::io::{self, Cursor};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
const REPO_URL: &str = "https://github.com/PrismarineJS/minecraft-data";
const BRANCH: &str = "master";
const DATA_PREFIX_IN_ZIP: &str = "minecraft-data-master/data/";
const CACHE_SUBDIR: &str = "mcdata-rs";
const DATA_DIR_NAME: &str = "minecraft-data";
static DOWNLOAD_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
static DATA_PATH: OnceCell<PathBuf> = OnceCell::new();
pub fn get_data_root() -> Result<&'static Path, McDataError> {
DATA_PATH.get_or_try_init(|| {
let _lock = DOWNLOAD_LOCK.lock().map_err(|_| {
McDataError::Internal("Failed to acquire download lock".to_string())
})?;
if let Some(path) = DATA_PATH.get() {
log::trace!("Data path already initialized by another thread: {}", path.display());
return Ok(path.clone());
}
let base_cache_dir = dirs_next::cache_dir()
.ok_or_else(|| McDataError::CacheDirNotFound)?
.join(CACHE_SUBDIR); let target_repo_dir = base_cache_dir.join(DATA_DIR_NAME); let target_data_dir = target_repo_dir.join("data");
let check_file = target_data_dir.join("dataPaths.json");
if target_data_dir.is_dir() && check_file.is_file() {
log::info!("Found existing minecraft-data at: {}", target_data_dir.display());
Ok(target_data_dir)
} else {
log::info!(
"minecraft-data not found or incomplete at {}. Downloading...",
target_data_dir.display()
);
fs::create_dir_all(&base_cache_dir).map_err(|e| McDataError::IoError {
path: base_cache_dir.clone(),
source: e,
})?;
download_and_extract(&target_repo_dir)?;
if target_data_dir.is_dir() && check_file.is_file() {
log::info!("Successfully downloaded and extracted data to {}", target_data_dir.display());
Ok(target_data_dir)
} else {
log::error!("Verification failed after download. Expected data directory not found or incomplete: {}", target_data_dir.display());
Err(McDataError::DownloadVerificationFailed(target_data_dir))
}
}
}).map(|p| p.as_path())
}
fn download_and_extract(target_base_dir: &Path) -> Result<(), McDataError> {
let download_url = format!("{}/archive/refs/heads/{}.zip", REPO_URL, BRANCH);
log::debug!("Downloading from {}", download_url);
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(300)) .user_agent(format!("mcdata-rs/{}", env!("CARGO_PKG_VERSION"))) .build()
.map_err(|e| McDataError::DownloadError(e.to_string()))?;
let response = client
.get(&download_url)
.send()
.map_err(|e| McDataError::DownloadError(format!("Request failed: {}", e)))?;
if !response.status().is_success() {
return Err(McDataError::DownloadError(format!(
"Download failed with status: {}",
response.status()
)));
}
let zip_data = response
.bytes()
.map_err(|e| McDataError::DownloadError(format!("Failed to read response bytes: {}", e)))?;
log::debug!(
"Download complete ({} bytes). Extracting...",
zip_data.len()
);
let reader = Cursor::new(zip_data); let mut archive = zip::ZipArchive::new(reader)
.map_err(|e| McDataError::ArchiveError(format!("Failed to open zip archive: {}", e)))?;
if target_base_dir.exists() {
log::debug!("Removing existing directory: {}", target_base_dir.display());
fs::remove_dir_all(target_base_dir).map_err(|e| McDataError::IoError {
path: target_base_dir.to_path_buf(),
source: e,
})?;
}
fs::create_dir_all(target_base_dir).map_err(|e| McDataError::IoError {
path: target_base_dir.to_path_buf(),
source: e,
})?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| {
McDataError::ArchiveError(format!("Failed to get file at index {}: {}", i, e))
})?;
let full_path_in_zip = match file.enclosed_name() {
Some(path) => path.to_owned(),
None => {
log::warn!(
"Skipping entry with potentially unsafe path in zip: {}",
file.name()
);
continue; }
};
if !full_path_in_zip.starts_with(DATA_PREFIX_IN_ZIP) {
continue;
}
let relative_path_str = full_path_in_zip.to_str().ok_or_else(|| {
McDataError::Internal(format!(
"Non-UTF8 path in zip: {}",
full_path_in_zip.display()
))
})?;
if !relative_path_str.starts_with(DATA_PREFIX_IN_ZIP) {
log::warn!(
"Path {} does not start with expected prefix {}, skipping.",
relative_path_str,
DATA_PREFIX_IN_ZIP
);
continue;
}
let relative_path = Path::new(&relative_path_str[DATA_PREFIX_IN_ZIP.len()..]);
let outpath = target_base_dir.join("data").join(relative_path);
if file.name().ends_with('/') {
log::trace!("Creating directory {}", outpath.display());
fs::create_dir_all(&outpath).map_err(|e| McDataError::IoError {
path: outpath.clone(),
source: e,
})?;
} else {
log::trace!("Extracting file to {}", outpath.display());
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p).map_err(|e| McDataError::IoError {
path: p.to_path_buf(),
source: e,
})?;
}
}
let mut outfile = File::create(&outpath).map_err(|e| McDataError::IoError {
path: outpath.clone(),
source: e,
})?;
io::copy(&mut file, &mut outfile).map_err(|e| McDataError::IoError {
path: outpath.clone(),
source: e,
})?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
if mode != 0 {
if let Err(e) = fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))
{
log::warn!("Failed to set permissions on {}: {}", outpath.display(), e);
}
}
}
}
}
log::debug!("Extraction complete.");
Ok(())
}