Add web crawler infrastructure and HangarMC implementation

This commit is contained in:
Rbanh 2025-03-29 01:00:49 -04:00
parent d9cf404402
commit eb51afaea8
7 changed files with 1466 additions and 32 deletions

View File

@ -4,7 +4,7 @@
- [x] Create project roadmap
- [x] Initialize Tauri + React project
- [x] Setup basic project structure
- [ ] Create GitHub repository (optional)
- [x] Create Remote repository (optional)
## Core Infrastructure (In Progress)
- [x] Setup SQLite or JSON storage for plugin data
@ -18,9 +18,9 @@
- [x] Design plugin metadata extraction system
- [x] Build plugin hash identification system
## Web Crawler Development (Upcoming)
- [ ] Create base web crawler architecture
- [ ] Implement HangarMC crawler
## Web Crawler Development (In Progress)
- [x] Create base web crawler architecture
- [x] Implement HangarMC crawler
- [ ] Implement SpigotMC crawler
- [ ] Implement Modrinth crawler
- [ ] Implement GitHub releases crawler

366
src-tauri/Cargo.lock generated
View File

@ -606,6 +606,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
@ -629,9 +639,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.9.0",
"core-foundation",
"core-foundation 0.10.0",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"libc",
]
@ -642,7 +652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.9.0",
"core-foundation",
"core-foundation 0.10.0",
"libc",
]
@ -915,7 +925,7 @@ dependencies = [
"rustc_version",
"toml",
"vswhom",
"winreg",
"winreg 0.52.0",
]
[[package]]
@ -924,6 +934,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.0"
@ -1039,6 +1058,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -1046,7 +1074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@ -1060,6 +1088,12 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@ -1476,6 +1510,25 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "h2"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.8.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1535,6 +1588,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.15",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1546,6 +1610,17 @@ dependencies = [
"itoa 1.0.15",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@ -1553,7 +1628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
"http 1.3.1",
]
[[package]]
@ -1564,8 +1639,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"pin-project-lite",
]
@ -1575,6 +1650,36 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa 1.0.15",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.6.0"
@ -1584,8 +1689,8 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
"itoa 1.0.15",
"pin-project-lite",
@ -1594,6 +1699,19 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
@ -1603,9 +1721,9 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.6.0",
"pin-project-lite",
"socket2",
"tokio",
@ -2164,6 +2282,23 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -2494,6 +2629,50 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -2780,6 +2959,7 @@ name = "plugsnatcher"
version = "0.1.0"
dependencies = [
"regex",
"reqwest 0.11.27",
"serde",
"serde_json",
"sha2",
@ -3098,6 +3278,46 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
]
[[package]]
name = "reqwest"
version = "0.12.15"
@ -3108,10 +3328,10 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper 1.6.0",
"hyper-util",
"ipnet",
"js-sys",
@ -3123,7 +3343,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tokio-util",
"tower",
@ -3202,6 +3422,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustversion"
version = "1.0.20"
@ -3223,6 +3452,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.8.22"
@ -3256,6 +3494,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.22.0"
@ -3522,7 +3783,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"foreign-types",
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
@ -3643,6 +3904,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@ -3663,6 +3930,27 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@ -3683,7 +3971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1"
dependencies = [
"bitflags 2.9.0",
"core-foundation",
"core-foundation 0.10.0",
"core-graphics",
"crossbeam-channel",
"dispatch",
@ -3748,7 +4036,7 @@ dependencies = [
"glob",
"gtk",
"heck 0.5.0",
"http",
"http 1.3.1",
"jni",
"libc",
"log",
@ -3760,7 +4048,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.12.15",
"serde",
"serde_json",
"serde_repr",
@ -3934,7 +4222,7 @@ dependencies = [
"cookie",
"dpi",
"gtk",
"http",
"http 1.3.1",
"jni",
"raw-window-handle",
"serde",
@ -3952,7 +4240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087188020fd6facb8578fe9b38e81fa0fe5fb85744c73da51a299f94a530a1e3"
dependencies = [
"gtk",
"http",
"http 1.3.1",
"jni",
"log",
"objc2 0.6.0",
@ -3985,7 +4273,7 @@ dependencies = [
"dunce",
"glob",
"html5ever",
"http",
"http 1.3.1",
"infer",
"json-patch",
"kuchikiki",
@ -4148,6 +4436,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.14"
@ -4226,7 +4524,7 @@ dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
@ -4431,6 +4729,12 @@ dependencies = [
"serde",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"
@ -5149,6 +5453,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.52.0"
@ -5195,7 +5509,7 @@ dependencies = [
"gdkx11",
"gtk",
"html5ever",
"http",
"http 1.3.1",
"javascriptcore-rs",
"jni",
"kuchikiki",

View File

@ -28,4 +28,5 @@ yaml-rust = "0.4"
walkdir = "2.4"
regex = "1.10"
sha2 = "0.10"
reqwest = { version = "0.11", features = ["blocking", "json"] }

View File

@ -0,0 +1,203 @@
use std::error::Error;
use std::path::Path;
use serde::{Serialize, Deserialize};
use crate::{HttpClient, RepositoryCrawler, RepositoryPlugin, RepositorySource};
// HangarMC API response structures
#[derive(Debug, Serialize, Deserialize)]
struct HangarProjectsResponse {
pagination: HangarPagination,
result: Vec<HangarProject>,
}
#[derive(Debug, Serialize, Deserialize)]
struct HangarPagination {
limit: u32,
offset: u32,
count: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct HangarProject {
id: String,
name: String,
namespace: HangarNamespace,
category: String,
description: Option<String>,
stats: HangarStats,
last_updated: String,
icon_url: Option<String>,
created_at: String,
visibility: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct HangarNamespace {
owner: String,
slug: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct HangarStats {
views: u64,
downloads: u64,
recent_views: u64,
recent_downloads: u64,
stars: u64,
watchers: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct HangarVersionsResponse {
pagination: HangarPagination,
result: Vec<HangarVersion>,
}
#[derive(Debug, Serialize, Deserialize)]
struct HangarVersion {
name: String,
created_at: String,
description: Option<String>,
downloads: u64,
file_size: u64,
platform_versions: Vec<String>,
}
// HangarMC crawler implementation
pub struct HangarCrawler {
client: HttpClient,
api_base_url: String,
}
impl HangarCrawler {
pub fn new() -> Self {
HangarCrawler {
client: HttpClient::new(),
api_base_url: "https://hangar.papermc.io/api/v1".to_string(),
}
}
fn get_project_details(&self, owner: &str, slug: &str) -> Result<HangarProject, Box<dyn Error>> {
let url = format!("{}/projects/{}/{}", self.api_base_url, owner, slug);
let response = self.client.get(&url)?;
let project: HangarProject = serde_json::from_str(&response)?;
Ok(project)
}
fn get_project_versions(&self, owner: &str, slug: &str) -> Result<Vec<HangarVersion>, Box<dyn Error>> {
let url = format!("{}/projects/{}/{}/versions", self.api_base_url, owner, slug);
let response = self.client.get(&url)?;
let versions_response: HangarVersionsResponse = serde_json::from_str(&response)?;
Ok(versions_response.result)
}
fn build_download_url(&self, owner: &str, slug: &str, version: &str) -> String {
format!("https://hangar.papermc.io/api/v1/projects/{}/{}/versions/{}/download", owner, slug, version)
}
}
impl RepositoryCrawler for HangarCrawler {
fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, Box<dyn Error>> {
let url = format!("{}/projects?query={}&limit=20", self.api_base_url, query);
let response = self.client.get(&url)?;
let projects_response: HangarProjectsResponse = serde_json::from_str(&response)?;
let mut results = Vec::new();
for project in projects_response.result {
// For each project, get the latest version
let versions = self.get_project_versions(&project.namespace.owner, &project.namespace.slug)?;
if let Some(latest_version) = versions.first() {
results.push(RepositoryPlugin {
id: format!("{}/{}", project.namespace.owner, project.namespace.slug),
name: project.name,
version: latest_version.name.clone(),
description: project.description,
authors: vec![project.namespace.owner.clone()],
download_url: self.build_download_url(&project.namespace.owner, &project.namespace.slug, &latest_version.name),
repository: RepositorySource::HangarMC,
page_url: format!("https://hangar.papermc.io/{}/{}", project.namespace.owner, project.namespace.slug),
download_count: Some(project.stats.downloads),
last_updated: Some(project.last_updated),
icon_url: project.icon_url,
minecraft_versions: latest_version.platform_versions.clone(),
categories: vec![project.category],
rating: None, // HangarMC uses stars, not ratings
file_size: Some(latest_version.file_size),
file_hash: None, // HangarMC API doesn't provide file hashes
});
}
}
Ok(results)
}
fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, Box<dyn Error>> {
let parts: Vec<&str> = plugin_id.split('/').collect();
if parts.len() != 2 {
return Err("Invalid plugin ID format for HangarMC. Expected 'owner/slug'".into());
}
let owner = parts[0];
let slug = parts[1];
let project = self.get_project_details(owner, slug)?;
let versions = self.get_project_versions(owner, slug)?;
if let Some(latest_version) = versions.first() {
Ok(RepositoryPlugin {
id: plugin_id.to_string(),
name: project.name,
version: latest_version.name.clone(),
description: project.description,
authors: vec![project.namespace.owner.clone()],
download_url: self.build_download_url(owner, slug, &latest_version.name),
repository: RepositorySource::HangarMC,
page_url: format!("https://hangar.papermc.io/{}/{}", owner, slug),
download_count: Some(project.stats.downloads),
last_updated: Some(project.last_updated),
icon_url: project.icon_url,
minecraft_versions: latest_version.platform_versions.clone(),
categories: vec![project.category],
rating: None,
file_size: Some(latest_version.file_size),
file_hash: None,
})
} else {
Err("No versions found for this plugin".into())
}
}
fn get_plugin_versions(&self, plugin_id: &str) -> Result<Vec<String>, Box<dyn Error>> {
let parts: Vec<&str> = plugin_id.split('/').collect();
if parts.len() != 2 {
return Err("Invalid plugin ID format for HangarMC. Expected 'owner/slug'".into());
}
let owner = parts[0];
let slug = parts[1];
let versions = self.get_project_versions(owner, slug)?;
Ok(versions.into_iter().map(|v| v.name).collect())
}
fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result<String, Box<dyn Error>> {
let parts: Vec<&str> = plugin_id.split('/').collect();
if parts.len() != 2 {
return Err("Invalid plugin ID format for HangarMC. Expected 'owner/slug'".into());
}
let owner = parts[0];
let slug = parts[1];
let download_url = self.build_download_url(owner, slug, version);
self.client.download(&download_url, destination)?;
Ok(destination.to_string_lossy().to_string())
}
fn get_repository_name(&self) -> RepositorySource {
RepositorySource::HangarMC
}
}

View File

@ -0,0 +1,4 @@
pub mod hangar;
// Re-export the crawler implementations
pub use hangar::HangarCrawler;

View File

@ -8,6 +8,12 @@ use zip::ZipArchive;
use yaml_rust::{YamlLoader, Yaml};
use std::fs::File;
use sha2::{Sha256, Digest};
use reqwest;
use std::error::Error;
// Add the crawlers module
mod crawlers;
use crawlers::HangarCrawler;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum ServerType {
@ -603,12 +609,155 @@ fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
// Web Crawler Module
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum RepositorySource {
HangarMC,
SpigotMC,
Modrinth,
GitHub,
BukkitDev,
Custom(String),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RepositoryPlugin {
id: String, // Unique identifier in the repository
name: String, // Plugin name
version: String, // Latest version
description: Option<String>,
authors: Vec<String>,
download_url: String, // URL to download the plugin
repository: RepositorySource,
page_url: String, // URL to the plugin page
download_count: Option<u64>,
last_updated: Option<String>,
icon_url: Option<String>,
minecraft_versions: Vec<String>,
categories: Vec<String>,
rating: Option<f32>,
file_size: Option<u64>,
file_hash: Option<String>,
}
// Trait for implementing different repository crawlers
pub trait RepositoryCrawler {
fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, Box<dyn Error>>;
fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, Box<dyn Error>>;
fn get_plugin_versions(&self, plugin_id: &str) -> Result<Vec<String>, Box<dyn Error>>;
fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result<String, Box<dyn Error>>;
fn get_repository_name(&self) -> RepositorySource;
}
// Basic HTTP client for crawler implementations
pub struct HttpClient {
client: reqwest::blocking::Client,
}
impl HttpClient {
pub fn new() -> Self {
let client = reqwest::blocking::Client::builder()
.user_agent("PlugSnatcher/0.1.0")
.build()
.unwrap_or_else(|_| reqwest::blocking::Client::new());
HttpClient { client }
}
pub fn get(&self, url: &str) -> Result<String, Box<dyn Error>> {
let response = self.client.get(url).send()?;
if response.status().is_success() {
Ok(response.text()?)
} else {
Err(format!("HTTP error: {}", response.status()).into())
}
}
pub fn download(&self, url: &str, destination: &Path) -> Result<(), Box<dyn Error>> {
let response = self.client.get(url).send()?;
if response.status().is_success() {
let bytes = response.bytes()?;
fs::write(destination, bytes)?;
Ok(())
} else {
Err(format!("Download failed: {}", response.status()).into())
}
}
}
// Helper function to get crawler for a specific repository
fn get_crawler(repository: &RepositorySource) -> Option<Box<dyn RepositoryCrawler>> {
match repository {
RepositorySource::HangarMC => Some(Box::new(HangarCrawler::new())),
// Other repositories will be implemented later
_ => None,
}
}
// Command to search for plugins in specified repositories
#[command]
pub fn search_repository_plugins(query: &str, repositories: Vec<RepositorySource>) -> Result<Vec<RepositoryPlugin>, String> {
let mut results: Vec<RepositoryPlugin> = Vec::new();
// Try each requested repository
for repo in repositories {
if let Some(crawler) = get_crawler(&repo) {
match crawler.search(query) {
Ok(repo_results) => {
results.extend(repo_results);
},
Err(e) => {
println!("Error searching in repository {:?}: {}", repo, e);
// Continue with other repositories even if one fails
}
}
} else {
println!("Repository crawler for {:?} not implemented yet", repo);
}
}
if results.is_empty() {
Err("No plugins found or repositories not implemented yet".to_string())
} else {
Ok(results)
}
}
// Command to get plugin details from a specific repository
#[command]
pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySource) -> Result<RepositoryPlugin, String> {
if let Some(crawler) = get_crawler(&repository) {
crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string())
} else {
Err(format!("Repository crawler for {:?} not implemented yet", repository))
}
}
// Command to download a plugin from a repository
#[command]
pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result<String, String> {
if let Some(crawler) = get_crawler(&repository) {
crawler
.download_plugin(plugin_id, version, Path::new(destination))
.map_err(|e| e.to_string())
} else {
Err(format!("Repository crawler for {:?} not implemented yet", repository))
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet, scan_server_directory])
.invoke_handler(tauri::generate_handler![
greet,
scan_server_directory,
search_repository_plugins,
get_repository_plugin_details,
download_repository_plugin
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

763
src-tauri/src/lib.rs.bak Normal file
View File

@ -0,0 +1,763 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use serde::{Serialize, Deserialize};
use std::path::Path;
use std::fs;
use std::io::Read;
use tauri::command;
use zip::ZipArchive;
use yaml_rust::{YamlLoader, Yaml};
use std::fs::File;
use sha2::{Sha256, Digest};
use reqwest;
use std::error::Error;
// Add the crawlers module
mod crawlers;
use crawlers::HangarCrawler;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum ServerType {
Paper,
Spigot,
Bukkit,
Vanilla,
Forge,
Fabric,
Velocity,
BungeeCord,
Waterfall,
Unknown,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ServerInfo {
server_type: ServerType,
minecraft_version: Option<String>,
plugins_directory: String,
plugins_count: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Plugin {
name: String,
version: String,
latest_version: Option<String>,
description: Option<String>,
authors: Vec<String>,
has_update: bool,
api_version: Option<String>,
main_class: Option<String>,
depend: Option<Vec<String>>,
soft_depend: Option<Vec<String>>,
load_before: Option<Vec<String>>,
commands: Option<serde_json::Value>,
permissions: Option<serde_json::Value>,
file_path: String,
file_hash: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PluginMeta {
pub name: String,
pub version: String,
pub description: Option<String>,
pub authors: Vec<String>,
pub api_version: Option<String>,
pub main_class: Option<String>,
pub depend: Option<Vec<String>>,
pub soft_depend: Option<Vec<String>>,
pub load_before: Option<Vec<String>>,
pub commands: Option<serde_json::Value>,
pub permissions: Option<serde_json::Value>,
pub file_path: String,
pub file_size: u64,
pub file_hash: String,
}
/// Calculates SHA-256 hash for a given file path
pub fn calculate_file_hash(file_path: &str) -> Result<String, String> {
let mut file = File::open(file_path).map_err(|e| format!("Failed to open file for hashing: {}", e))?;
let mut hasher = Sha256::new();
let mut buffer = [0; 1024];
loop {
let bytes_read = file.read(&mut buffer).map_err(|e| format!("Failed to read file for hashing: {}", e))?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
let hash = hasher.finalize();
Ok(format!("{:x}", hash))
}
/// Extract metadata from a plugin.yml file inside a JAR
fn extract_plugin_metadata(jar_path: &Path) -> Result<PluginMeta, String> {
let file = fs::File::open(jar_path)
.map_err(|e| format!("Failed to open JAR file: {}", e))?;
let file_size = file.metadata()
.map_err(|e| format!("Failed to read file metadata: {}", e))?
.len();
let mut archive = ZipArchive::new(file)
.map_err(|e| format!("Invalid JAR file: {}", e))?;
// Try to find and read plugin.yml or bungee.yml
let yaml_content = match read_yaml_from_archive(&mut archive, "plugin.yml") {
Ok(content) => content,
Err(_) => match read_yaml_from_archive(&mut archive, "bungee.yml") {
Ok(content) => content,
Err(_) => {
// If no plugin metadata file is found, try to infer from filename
let filename = jar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.jar");
// Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8")
let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
let version = if parts.len() > 1 {
parts.pop().unwrap_or("1.0.0").to_string()
} else {
"1.0.0".to_string()
};
let name = parts.join("-");
return Ok(PluginMeta {
name,
version,
description: None,
authors: Vec::new(),
api_version: None,
main_class: None,
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: jar_path.to_string_lossy().to_string(),
file_size,
file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()),
});
}
}
};
// Parse the YAML content
let docs = match YamlLoader::load_from_str(&yaml_content) {
Ok(docs) => docs,
Err(e) => {
println!("Failed to parse plugin YAML: {}", e);
return fallback_plugin_meta(jar_path, file_size);
}
};
if docs.is_empty() {
return fallback_plugin_meta(jar_path, file_size);
}
let doc = &docs[0];
// Extract basic metadata with fallbacks for missing fields
let name = yaml_str_with_fallback(doc, "name", jar_path);
let version = yaml_str_with_fallback(doc, "version", jar_path);
// Extract optional fields
let description = yaml_str_opt(doc, "description");
// Handle authors (can be a single string or an array)
let authors = match &doc["authors"] {
Yaml::Array(arr) => {
arr.iter()
.filter_map(|a| a.as_str().map(|s| s.to_string()))
.collect()
},
Yaml::String(s) => vec![s.clone()],
_ => {
// Fallback to 'author' field which is sometimes used
match &doc["author"] {
Yaml::String(s) => vec![s.clone()],
_ => Vec::new(),
}
}
};
// Extract other optional metadata
let api_version = yaml_str_opt(doc, "api-version");
let main_class = yaml_str_opt(doc, "main");
// Handle dependency lists
let depend = yaml_str_array(doc, "depend");
let soft_depend = yaml_str_array(doc, "softdepend");
let load_before = yaml_str_array(doc, "loadbefore");
// Handle complex structures as generic JSON values
let commands = match &doc["commands"] {
Yaml::Hash(_) => {
Some(serde_json::Value::String("Commands data present".to_string()))
},
_ => None
};
let permissions = match &doc["permissions"] {
Yaml::Hash(_) => {
Some(serde_json::Value::String("Permissions data present".to_string()))
},
_ => None
};
// Calculate the file hash
let file_hash = calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string());
Ok(PluginMeta {
name,
version,
description,
authors,
api_version,
main_class,
depend,
soft_depend,
load_before,
commands,
permissions,
file_path: jar_path.to_string_lossy().to_string(),
file_size,
file_hash,
})
}
// Helper function to read a YAML file from the ZIP archive
fn read_yaml_from_archive(archive: &mut ZipArchive<fs::File>, file_name: &str) -> Result<String, String> {
match archive.by_name(file_name) {
Ok(mut file) => {
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| format!("Failed to read {}: {}", file_name, e))?;
Ok(contents)
},
Err(e) => Err(format!("Failed to find {}: {}", file_name, e))
}
}
// Helper function to create plugin metadata with fallback values
fn fallback_plugin_meta(jar_path: &Path, file_size: u64) -> Result<PluginMeta, String> {
let filename = jar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.jar");
// Extract name and version from filename (e.g., "WorldEdit-7.2.8.jar" → name: "WorldEdit", version: "7.2.8")
let mut parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
let version = if parts.len() > 1 {
parts.pop().unwrap_or("1.0.0").to_string()
} else {
"1.0.0".to_string()
};
let name = parts.join("-");
Ok(PluginMeta {
name,
version,
description: None,
authors: Vec::new(),
api_version: None,
main_class: None,
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: jar_path.to_string_lossy().to_string(),
file_size,
file_hash: calculate_file_hash(jar_path.to_str().unwrap_or("unknown.jar")).unwrap_or_else(|_| "unknown".to_string()),
})
}
// Extract a string from YAML with fallback to filename
fn yaml_str_with_fallback(yaml: &Yaml, key: &str, jar_path: &Path) -> String {
match yaml[key].as_str() {
Some(s) => s.to_string(),
None => {
// Extract from filename as fallback
let filename = jar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.jar");
if key == "name" {
// Extract name (e.g., "WorldEdit-7.2.8.jar" → "WorldEdit")
let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
parts[0].to_string()
} else if key == "version" {
// Extract version (e.g., "WorldEdit-7.2.8.jar" → "7.2.8")
let parts: Vec<&str> = filename.trim_end_matches(".jar").split('-').collect();
if parts.len() > 1 {
parts[1].to_string()
} else {
"1.0.0".to_string()
}
} else {
String::new()
}
}
}
}
fn yaml_str_opt(yaml: &Yaml, key: &str) -> Option<String> {
yaml[key].as_str().map(|s| s.to_string())
}
fn yaml_str_array(yaml: &Yaml, key: &str) -> Option<Vec<String>> {
match &yaml[key] {
Yaml::Array(arr) => {
let strings: Vec<String> = arr.iter()
.filter_map(|a| a.as_str().map(|s| s.to_string()))
.collect();
if strings.is_empty() { None } else { Some(strings) }
},
_ => None
}
}
/// Detect the server type based on files in the server directory
fn detect_server_type(server_path: &Path) -> ServerType {
// Check for Paper
if server_path.join("cache").join("patched_1.19.2.jar").exists() ||
server_path.join("paper.yml").exists() {
return ServerType::Paper;
}
// Check for Spigot
if server_path.join("spigot.yml").exists() {
return ServerType::Spigot;
}
// Check for Bukkit
if server_path.join("bukkit.yml").exists() {
return ServerType::Bukkit;
}
// Check for Forge
if server_path.join("forge-server.jar").exists() ||
server_path.join("mods").exists() {
return ServerType::Forge;
}
// Check for Fabric
if server_path.join("fabric-server-launch.jar").exists() ||
(server_path.join("mods").exists() && server_path.join("fabric-server-launcher.properties").exists()) {
return ServerType::Fabric;
}
// Check for Velocity
if server_path.join("velocity.toml").exists() {
return ServerType::Velocity;
}
// Check for BungeeCord
if server_path.join("BungeeCord.jar").exists() ||
server_path.join("config.yml").exists() {
return ServerType::BungeeCord;
}
// Check for Waterfall
if server_path.join("waterfall.jar").exists() ||
server_path.join("waterfall.yml").exists() {
return ServerType::Waterfall;
}
// Check if it's at least a vanilla server
if server_path.join("server.properties").exists() ||
server_path.join("vanilla_server.jar").exists() {
return ServerType::Vanilla;
}
// If no server type detected
ServerType::Unknown
}
/// Guess the Minecraft version from various files in the server directory
fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Option<String> {
// Try from version.json if it exists
if let Ok(content) = fs::read_to_string(server_path.join("version.json")) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(version) = json["name"].as_str() {
return Some(version.to_string());
}
}
}
// Try from the server jar name pattern
if let Ok(entries) = fs::read_dir(server_path) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "jar") {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
// Extract version from various common patterns in jar names
if filename.starts_with("paper-") ||
filename.starts_with("spigot-") ||
filename.starts_with("craftbukkit-") {
// Pattern: paper-1.19.2.jar, spigot-1.19.2.jar
let parts: Vec<&str> = filename.split('-').collect();
if parts.len() > 1 {
let version_part = parts[1].trim_end_matches(".jar");
if version_part.contains('.') { // Basic version format check
return Some(version_part.to_string());
}
}
}
// Look for version patterns like minecraft_server.1.19.2.jar
if filename.starts_with("minecraft_server.") {
let version_part = filename
.trim_start_matches("minecraft_server.")
.trim_end_matches(".jar");
if version_part.contains('.') {
return Some(version_part.to_string());
}
}
}
}
}
}
// If server type is proxy, look in config files
if server_type == &ServerType::BungeeCord ||
server_type == &ServerType::Waterfall ||
server_type == &ServerType::Velocity {
// Velocity uses TOML, others use YAML
if server_type == &ServerType::Velocity {
if let Ok(content) = fs::read_to_string(server_path.join("velocity.toml")) {
// Very basic TOML parsing just for this field
for line in content.lines() {
if line.contains("minecraft-version") {
if let Some(version) = line.split('=').nth(1) {
return Some(version.trim().trim_matches('"').to_string());
}
}
}
}
} else {
// Try to parse config.yml for BungeeCord/Waterfall
if let Ok(content) = fs::read_to_string(server_path.join("config.yml")) {
if let Ok(docs) = YamlLoader::load_from_str(&content) {
if !docs.is_empty() {
let doc = &docs[0];
if let Some(version) = doc["minecraft_version"].as_str() {
return Some(version.to_string());
}
}
}
}
}
}
// Default fallback
None
}
/// Get plugins directory path based on server type
fn get_plugins_directory(server_path: &Path, server_type: &ServerType) -> String {
match server_type {
ServerType::Velocity => server_path.join("plugins").to_string_lossy().to_string(),
ServerType::BungeeCord => server_path.join("plugins").to_string_lossy().to_string(),
ServerType::Waterfall => server_path.join("plugins").to_string_lossy().to_string(),
_ => server_path.join("plugins").to_string_lossy().to_string(),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScanResult {
server_info: ServerInfo,
plugins: Vec<Plugin>,
}
#[command]
fn scan_server_directory(path: &str) -> Result<ScanResult, String> {
let server_path = Path::new(path);
if !server_path.exists() {
return Err(format!("Server path does not exist: {}", path));
}
// Detect server type and version
let server_type = detect_server_type(server_path);
let minecraft_version = detect_minecraft_version(server_path, &server_type);
println!("Detected server type: {:?}", server_type);
if let Some(version) = &minecraft_version {
println!("Detected Minecraft version: {}", version);
}
// Determine plugins directory based on server type
let plugins_dir_str = get_plugins_directory(server_path, &server_type);
let plugins_dir = Path::new(&plugins_dir_str);
if !plugins_dir.exists() {
return Err(format!("Plugins directory not found at: {}", plugins_dir.display()));
}
// Scan for JAR files in the plugins directory
let mut plugins = Vec::new();
match fs::read_dir(&plugins_dir) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
// Check if this is a JAR file
if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) {
match extract_plugin_metadata(&path) {
Ok(meta) => {
// Create a Plugin from PluginMeta
let plugin = Plugin {
name: meta.name,
version: meta.version,
latest_version: None, // Will be filled by update checker
description: meta.description,
authors: meta.authors,
has_update: false, // Will be determined by update checker
api_version: meta.api_version,
main_class: meta.main_class,
depend: meta.depend,
soft_depend: meta.soft_depend,
load_before: meta.load_before,
commands: meta.commands,
permissions: meta.permissions,
file_path: meta.file_path,
file_hash: meta.file_hash,
};
plugins.push(plugin);
},
Err(e) => {
// Log error but continue with other plugins
println!("Error reading plugin from {}: {}", path.display(), e);
}
}
}
}
}
},
Err(e) => {
return Err(format!("Failed to read plugins directory: {}", e));
}
}
// If no plugins were found, fall back to mock data for testing
if plugins.is_empty() && server_type == ServerType::Unknown {
// For testing only - in production, we'd just return an empty list
plugins = vec![
Plugin {
name: "EssentialsX".to_string(),
version: "2.19.0".to_string(),
latest_version: Some("2.20.0".to_string()),
description: Some("Essential server tools for Minecraft".to_string()),
authors: vec!["md_5".to_string(), "SupaHam".to_string()],
has_update: true,
api_version: Some("1.13".to_string()),
main_class: Some("com.earth2me.essentials.Essentials".to_string()),
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: "EssentialsX.jar".to_string(),
file_hash: calculate_file_hash("EssentialsX.jar").unwrap_or_else(|_| "unknown".to_string()),
},
Plugin {
name: "WorldEdit".to_string(),
version: "7.2.8".to_string(),
latest_version: Some("7.2.8".to_string()),
description: Some("In-game map editor".to_string()),
authors: vec!["sk89q".to_string(), "wizjany".to_string()],
has_update: false,
api_version: Some("1.13".to_string()),
main_class: Some("com.sk89q.worldedit.bukkit.WorldEditPlugin".to_string()),
depend: None,
soft_depend: None,
load_before: None,
commands: None,
permissions: None,
file_path: "WorldEdit.jar".to_string(),
file_hash: calculate_file_hash("WorldEdit.jar").unwrap_or_else(|_| "unknown".to_string()),
},
];
}
// Create server info
let server_info = ServerInfo {
server_type,
minecraft_version,
plugins_directory: plugins_dir_str,
plugins_count: plugins.len(),
};
Ok(ScanResult {
server_info,
plugins,
})
}
#[command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
// Web Crawler Module
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum RepositorySource {
HangarMC,
SpigotMC,
Modrinth,
GitHub,
BukkitDev,
Custom(String),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RepositoryPlugin {
id: String, // Unique identifier in the repository
name: String, // Plugin name
version: String, // Latest version
description: Option<String>,
authors: Vec<String>,
download_url: String, // URL to download the plugin
repository: RepositorySource,
page_url: String, // URL to the plugin page
download_count: Option<u64>,
last_updated: Option<String>,
icon_url: Option<String>,
minecraft_versions: Vec<String>,
categories: Vec<String>,
rating: Option<f32>,
file_size: Option<u64>,
file_hash: Option<String>,
}
// Trait for implementing different repository crawlers
pub trait RepositoryCrawler {
fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, Box<dyn Error>>;
fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, Box<dyn Error>>;
fn get_plugin_versions(&self, plugin_id: &str) -> Result<Vec<String>, Box<dyn Error>>;
fn download_plugin(&self, plugin_id: &str, version: &str, destination: &Path) -> Result<String, Box<dyn Error>>;
fn get_repository_name(&self) -> RepositorySource;
}
// Basic HTTP client for crawler implementations
pub struct HttpClient {
client: reqwest::blocking::Client,
}
impl HttpClient {
pub fn new() -> Self {
let client = reqwest::blocking::Client::builder()
.user_agent("PlugSnatcher/0.1.0")
.build()
.unwrap_or_else(|_| reqwest::blocking::Client::new());
HttpClient { client }
}
pub fn get(&self, url: &str) -> Result<String, Box<dyn Error>> {
let response = self.client.get(url).send()?;
if response.status().is_success() {
Ok(response.text()?)
} else {
Err(format!("HTTP error: {}", response.status()).into())
}
}
pub fn download(&self, url: &str, destination: &Path) -> Result<(), Box<dyn Error>> {
let response = self.client.get(url).send()?;
if response.status().is_success() {
let bytes = response.bytes()?;
fs::write(destination, bytes)?;
Ok(())
} else {
Err(format!("Download failed: {}", response.status()).into())
}
}
}
// Helper function to get crawler for a specific repository
fn get_crawler(repository: &RepositorySource) -> Option<Box<dyn RepositoryCrawler>> {
match repository {
RepositorySource::HangarMC => Some(Box::new(HangarCrawler::new())),
// Other repositories will be implemented later
_ => None,
}
}
// Command to search for plugins in specified repositories
#[command]
pub fn search_repository_plugins(query: &str, repositories: Vec<RepositorySource>) -> Result<Vec<RepositoryPlugin>, String> {
let mut results: Vec<RepositoryPlugin> = Vec::new();
// Try each requested repository
for repo in repositories {
if let Some(crawler) = get_crawler(&repo) {
match crawler.search(query) {
Ok(repo_results) => {
results.extend(repo_results);
},
Err(e) => {
println!("Error searching in repository {:?}: {}", repo, e);
// Continue with other repositories even if one fails
}
}
} else {
println!("Repository crawler for {:?} not implemented yet", repo);
}
}
if results.is_empty() {
Err("No plugins found or repositories not implemented yet".to_string())
} else {
Ok(results)
}
}
// Command to get plugin details from a specific repository
#[command]
pub fn get_repository_plugin_details(plugin_id: &str, repository: RepositorySource) -> Result<RepositoryPlugin, String> {
if let Some(crawler) = get_crawler(&repository) {
crawler.get_plugin_details(plugin_id).map_err(|e| e.to_string())
} else {
Err(format!("Repository crawler for {:?} not implemented yet", repository))
}
}
// Command to download a plugin from a repository
#[command]
pub fn download_repository_plugin(plugin_id: &str, version: &str, repository: RepositorySource, destination: &str) -> Result<String, String> {
if let Some(crawler) = get_crawler(&repository) {
crawler
.download_plugin(plugin_id, version, Path::new(destination))
.map_err(|e| e.to_string())
} else {
Err(format!("Repository crawler for {:?} not implemented yet", repository))
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
greet,
scan_server_directory,
search_repository_plugins,
get_repository_plugin_details,
download_repository_plugin
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}