//! Handlers for Mod metadata that can be displayed in a GUI mod list or exported as a mod pack #[allow(clippy::all)] use async_zip::tokio::read::fs::ZipFileReader; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sha2::Digest; use std::collections::HashMap; use std::path::{Path, PathBuf}; use tokio::io::AsyncReadExt; use crate::prelude::IOError; use crate::utils::http::{write_icon, FetchSemaphore, IoSemaphore}; use super::{Cluster, PackagePath}; // TODO: move all of this to prisma, it would be perfect for it // TODO: Curseforge, Modrinth, SkyClient integration /// Creates [`Package`] data for a given [`Cluster`] from on-device files and APIs. /// Paths must be the full paths and not relative paths. #[tracing::instrument(skip(paths, _cluster, _io_semaphore, _fetch_semaphore))] #[onelauncher_debug::debugger] pub async fn generate_context( _cluster: Cluster, paths: Vec, _cache_path: PathBuf, _io_semaphore: &IoSemaphore, _fetch_semaphore: &FetchSemaphore, // credentials: &Credentials, ) -> crate::Result> { let mut hashes = HashMap::new(); for path in paths { if !path.exists() { continue; } if let Some(ext) = path.extension() { if ext == "txt" { continue; } } let mut file = tokio::fs::File::open(path.clone()) .await .map_err(|e| IOError::with_path(e, &path))?; let mut buffer = [0u8; 4096]; let mut sha512 = sha2::Sha512::new(); loop { let read = file.read(&mut buffer).await.map_err(IOError::from)?; if read == 0 { break; } sha512.update(&buffer[..read]); } let hash = format!("{:x}", sha512.finalize()); hashes.insert(hash, path.clone()); } let result = HashMap::new(); // TODO(pauline): Finish this // let mut stream = tokio_stream::iter(result_packages); // while let Some((k, v)) = stream.next().await { // let k = PackagePath::from_path(k).await?; // result.insert(k, v); // } Ok(result) } /// Represents types of packages handled by the launcher. #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum PackageType { /// represents a mod jarfile Mod, /// represents a datapack file DataPack, /// represents a resourcepack file Resource, /// represents a shaderpack file Shader, } impl PackageType { /// attempt to get a [`PackageType`] from a loader string. pub fn from_loader(loaders: Vec) -> Option { if loaders .iter() .any(|x| ["fabric", "forge", "quilt"].contains(&&**x)) { Some(PackageType::Mod) } else if loaders.iter().any(|x| x == "datapack") { Some(PackageType::DataPack) } else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) { Some(PackageType::Shader) } else if loaders .iter() .any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x)) { Some(PackageType::Resource) } else { None } } pub fn from_parent(path: PathBuf) -> Option { let path = path.parent()?.file_name()?; match path.to_str()? { "mods" => Some(PackageType::Mod), "datapacks" => Some(PackageType::DataPack), "resourcepacks" => Some(PackageType::Resource), "shaderpacks" => Some(PackageType::Shader), _ => None, } } pub fn get_name(&self) -> &'static str { match self { PackageType::Mod => "mod", PackageType::DataPack => "datapack", PackageType::Resource => "resourcepack", PackageType::Shader => "shaderpack", } } pub fn get_folder(&self) -> &'static str { match self { PackageType::Mod => "mods", PackageType::DataPack => "datapacks", PackageType::Resource => "resourcepacks", PackageType::Shader => "shaderpacks", } } } /// A struct that represents a Package. #[derive(Serialize, Deserialize, Clone, Debug)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct Package { pub sha512: String, pub meta: PackageMetadata, pub file_name: String, pub disabled: bool, } /// Metadata that represents a [`Package`]. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum PackageMetadata { Managed { package: Box, version: Box, users: Box, update: Option>, incompatible: bool, }, Mapped { title: Option, description: Option, authors: Vec, version: Option, icon: Option, package_type: Option, }, Unknown, } /// Universal metadata for any managed package from a Mod distribution platform. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedPackage { // Core Metadata pub id: String, pub uid: Option, pub package_type: String, pub title: String, pub description: String, pub main: String, pub versions: Vec, pub game_versions: Vec, pub loaders: Vec, pub icon_url: Option, pub created: DateTime, pub updated: DateTime, pub client: PackageSide, pub server: PackageSide, pub downloads: u32, pub followers: u32, pub categories: Vec, pub optional_categories: Option>, } /// Universal managed package version of a package. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedVersion { pub id: String, pub package_id: String, pub author: String, pub name: String, pub featured: bool, pub version_id: String, pub changelog: String, pub changelog_url: Option, pub published: DateTime, pub downloads: u32, pub version_type: String, pub files: Vec, pub deps: Vec, pub game_versions: Vec, pub loaders: Vec, } /// Universal interface for managed package files. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedVersionFile { pub url: String, pub file_name: String, pub primary: bool, pub size: u32, pub file_type: Option, pub hashes: HashMap, } /// Universal interface for managed package dependencies. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedDependency { pub version_id: Option, pub package_id: Option, pub file_name: Option, pub dependency_type: PackageDependency, } /// Universal interface for managed package authors and users. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedUser { pub id: String, pub role: String, pub username: String, pub name: Option, pub avatar: Option, pub description: Option, pub created: DateTime, } /// The type of a [`ManagedDependency`]. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] pub enum PackageDependency { Required, Optional, Incompatible, Embedded, } /// The Client/Server side type of a [`Package`]. #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] // #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub enum PackageSide { Required, Optional, Unsupported, Unknown, } /// The file type of a [`Package`]. #[cfg_attr(feature = "tauri", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Debug)] // #[serde(rename_all = "kebab-case")] pub enum PackageFile { RequiredPack, OptionalPack, Unknown, } #[tracing::instrument(skip(io_semaphore))] #[onelauncher_debug::debugger] async fn read_icon( icon_path: Option, cache_path: &Path, path: &PathBuf, io_semaphore: &IoSemaphore, ) -> crate::Result> { if let Some(icon_path) = icon_path { let zip_reader = ZipFileReader::new(path).await; if let Ok(zip_reader) = zip_reader { let zip_idx_res = zip_reader .file() .entries() .iter() .position(|f| f.filename().as_str().unwrap_or_default() == icon_path); if let Some(zip_idx) = zip_idx_res { let mut bytes = Vec::new(); if zip_reader .reader_with_entry(zip_idx) .await? .read_to_end_checked(&mut bytes) .await .is_ok() { let bytes = bytes::Bytes::from(bytes); let path = write_icon(&icon_path, cache_path, bytes, io_semaphore).await?; return Ok(Some(path)); } } } } Ok(None) }