Skip to content

Commit

Permalink
Refactor code
Browse files Browse the repository at this point in the history
  • Loading branch information
aksiome committed Dec 29, 2024
1 parent 8b43ed3 commit b9af0b3
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 224 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
192 changes: 90 additions & 102 deletions src/api/download.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,56 @@
use std::collections::HashSet;
use std::io;
use std::io::Cursor;
use std::io::Write;
use std::io::{Cursor, Write};
use std::ops::Deref;
use std::path::Path;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use anyhow::{Context, Result};
use axum::body::Bytes;
use axum::extract::Query;
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use serde::Deserialize;
use cached::proc_macro::cached;
use reqwest::Client;
use tokio::fs::File;
use tokio::fs::OpenOptions;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use zip::write::SimpleFileOptions;
use serde::Deserialize;
use zip::CompressionMethod;
use zip::write::SimpleFileOptions;
use zip::ZipArchive;
use zip::ZipWriter;

use crate::manifest::v2::Module;
use crate::manifest::v2::ModuleKind;

use crate::manifest::ManifestKind;
use crate::manifest::v2::{Module, ModuleKind};
use crate::utils::{read_from_file, write_to_file};
use super::manifest::fetch_manifest;


type VersionedModules = Vec<(Module, String)>;


#[derive(Deserialize)]
pub struct QueryParams {
version: String,
modules: String,
}

pub async fn download(Query(params): Query<QueryParams>) -> impl IntoResponse {
let manifest = match fetch_manifest(params.version.clone()).await {
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR).into_response(),
Ok(None) => return (StatusCode::UNPROCESSABLE_ENTITY).into_response(),
Ok(Some(manifest)) => manifest,
};
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
struct VersionedModule {
module: Module,
version: String,
}

let mut modules = Vec::new();
let ids: Vec<&str> = params.modules.split(',').collect();
impl Deref for VersionedModule {
type Target = Module;

for module in manifest.into_latest().modules {
if let Some(id) = ids.iter().find(|&id| id.starts_with(&module.id)) {
modules.push(
if let Some(version) = id.split(':').nth(1) {
(module, version.to_string())
} else {
(module, params.version.to_string())
}
);
}
fn deref(&self) -> &Self::Target {
&self.module
}
}


pub async fn download(Query(params): Query<QueryParams>) -> impl IntoResponse {
let manifest = match fetch_manifest(params.version.to_owned()).await {
Ok(Some(manifest)) => manifest,
Ok(None) => return (StatusCode::UNPROCESSABLE_ENTITY).into_response(),
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR).into_response(),
};

let modules = get_versioned_modules(manifest, params).await;
if modules.is_empty() {
return (StatusCode::UNPROCESSABLE_ENTITY).into_response();
}
Expand All @@ -71,7 +62,7 @@ pub async fn download(Query(params): Query<QueryParams>) -> impl IntoResponse {
(header::CONTENT_TYPE, "application/zip"),
(
header::CONTENT_DISPOSITION,
&format!("attachment; filename=\"bookshelf-{}.zip\"", params.version),
"attachment; filename=\"bookshelf.zip\"",
),
];
(StatusCode::OK, headers, Bytes::from(data)).into_response()
Expand All @@ -80,79 +71,85 @@ pub async fn download(Query(params): Query<QueryParams>) -> impl IntoResponse {
}

#[cached(time = 60, result = true)]
async fn create_bundle(modules: VersionedModules) -> Result<Vec<u8>> {
if modules.is_empty() {
return Err(anyhow!("Cannot create an empty bundle"));
}

let (data_packs, resource_packs): (VersionedModules, VersionedModules) = modules
.clone()
async fn create_bundle(modules: Vec<VersionedModule>) -> Result<Vec<u8>> {
let (datapacks, resourcepacks): (Vec<VersionedModule>, Vec<VersionedModule>) = modules
.into_iter()
.partition(|(module, _)| matches!(module.kind, ModuleKind::DataPack));
.partition(|module| matches!(module.kind, ModuleKind::DataPack));

if data_packs.is_empty() || resource_packs.is_empty() {
create_specialized_bundle(modules).await
if datapacks.is_empty() || resourcepacks.is_empty() {
create_pack([datapacks, resourcepacks].concat()).await
} else {
let mut buffer = Vec::new();
let mut zip_writer = ZipWriter::new(Cursor::new(&mut buffer));
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);

zip_writer.start_file("resource_packs.zip", options)?;
zip_writer.write_all(&create_specialized_bundle(data_packs).await?)?;
zip_writer.start_file("data_packs.zip", options)?;
zip_writer.write_all(&create_specialized_bundle(resource_packs).await?)?;
zip_writer.finish()?;

Ok(buffer)
create_packs(datapacks, resourcepacks).await
}
}

async fn create_specialized_bundle(modules: VersionedModules) -> Result<Vec<u8>> {
async fn create_packs(
datapacks: Vec<VersionedModule>,
resourcepacks: Vec<VersionedModule>,
) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut zip_writer = ZipWriter::new(Cursor::new(&mut buffer));
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);

zip_writer.start_file("resource_packs.zip", options)?;
zip_writer.write_all(&create_pack(datapacks).await?)?;
zip_writer.start_file("data_packs.zip", options)?;
zip_writer.write_all(&create_pack(resourcepacks).await?)?;
zip_writer.finish()?;

Ok(buffer)
}

async fn create_pack(modules: Vec<VersionedModule>) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut writer = ZipWriter::new(Cursor::new(&mut buffer));

let mut duplicates = HashSet::new();
let options = SimpleFileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(9));

let mut duplicates = HashSet::new();
for module in modules {
let mut archive = fetch_module(module.0, module.1).await?;
let data = fetch_module(module.module, module.version).await?;
let mut archive = ZipArchive::new(Cursor::new(data))?;

for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let name = file.name().to_string();

if !duplicates.contains(&name) {
zip_writer.start_file(&name, options)?;
std::io::copy(&mut file, &mut zip_writer)?;
duplicates.insert(name);
if duplicates.insert(name.clone()) {
writer.start_file(&name, options)?;
std::io::copy(&mut file, &mut writer)?;
}
}
}

zip_writer.finish()?;
writer.finish()?;
Ok(buffer)
}

#[cached(time = 60, result = true)]
async fn fetch_module(module: Module, version: String) -> Result<ZipArchive<Cursor<Vec<u8>>>> {
if let Ok(bytes) = fetch_module_from_modrinth(&module, &version).await {
Ok(ZipArchive::new(Cursor::new(bytes))?)
} else if let Ok(bytes) = fetch_module_from_disk(&module, &version).await {
Ok(ZipArchive::new(Cursor::new(bytes))?)
} else {
Err(anyhow!("Failed to fetch module from all sources"))
async fn fetch_module(
module: Module,
version: String,
) -> Result<Vec<u8>> {
let disk_path = format!("data/{}/{}.zip", version, module.id);
match fetch_module_from_modrinth(&module, &version).await {
Ok(bytes) => {
if !Path::new(&disk_path).exists() {
write_to_file(&disk_path, &bytes).await?;
}
Ok(bytes)
},
Err(_) => read_from_file(&disk_path).await
.context("Failed to fetch module from all sources"),
}
}

async fn fetch_module_from_disk(module: &Module, version: &str) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let path = format!("data/{}/{}.zip", version, module.id);
File::open(path).await?.read_to_end(&mut buffer).await?;
Ok(buffer)
}

async fn fetch_module_from_modrinth(module: &Module, version: &str) -> Result<Vec<u8>> {
async fn fetch_module_from_modrinth(
module: &Module,
version: &str,
) -> Result<Vec<u8>> {
let client = Client::new();
let url = format!("https://api.modrinth.com/v3/project/{}/version/{}", module.slug, version);

Expand All @@ -161,28 +158,19 @@ async fn fetch_module_from_modrinth(module: &Module, version: &str) -> Result<Ve
let url = data["files"][0]["url"].as_str().context("Failed to find file URL in response")?;

let response = client.get(url).send().await?;
if !response.status().is_success() {
return Err(anyhow!("Failed to download the module from Modrinth: {}", response.status()));
}

let bytes = response.bytes().await?.to_vec();
write_module_to_disk(module, version, &bytes).await?;

Ok(bytes)
Ok(response.bytes().await?.to_vec())
}

async fn write_module_to_disk(module: &Module, version: &str, bytes: &[u8]) -> io::Result<()> {
let path = format!("data/{}/{}.zip", version, module.id);
if !Path::new(&path).exists() {
return Ok(())
}

let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
.await?;
async fn get_versioned_modules(
manifest: ManifestKind,
params: QueryParams,
) -> Vec<VersionedModule> {
let ids: Vec<&str> = params.modules.split(',').collect();

file.write_all(bytes).await
manifest.into_latest().modules.into_iter().filter_map(|module| {
ids.iter().find(|&id| id.starts_with(&module.id)).map(|id| {
let version = id.split(':').nth(1).unwrap_or(&params.version).to_owned();
VersionedModule {module, version}
})
}).collect()
}
78 changes: 27 additions & 51 deletions src/api/manifest.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,51 @@
use std::io;
use std::path::Path as StdPath;

use anyhow::{Context, Result};
use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json};
use axum::extract::Path;
use axum::http::StatusCode;
use axum::Json;
use axum::response::IntoResponse;
use cached::proc_macro::cached;
use reqwest::Client;
use tokio::{fs::{File, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt}};

use super::versions::{fetch_versions, Version};
use crate::manifest::ManifestKind;
use crate::utils::{read_from_json_file, write_to_json_file};
use super::versions::{fetch_versions, Version};


pub async fn manifest(Path(id): Path<String>) -> impl IntoResponse {
match fetch_manifest(id).await {
pub async fn manifest(Path(version): Path<String>) -> impl IntoResponse {
match fetch_manifest(version).await {
Ok(Some(data)) => Json(data.into_latest()).into_response(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
Ok(data) => match data {
Some(data) => Json(data.into_latest()).into_response(),
None => (StatusCode::NOT_FOUND).into_response(),
},
}
}

#[cached(time = 600, result = true)]
#[cached(time = 86400, result = true)]
pub async fn fetch_manifest(version: String) -> Result<Option<ManifestKind>> {
let versions = fetch_versions().await.context("Failed to fetch versions")?;
match versions.into_iter().find(|entry| entry.version == version) {
None => Ok(None),
Some(version) => {
if let Ok(manifest) = fetch_manifest_from_github(&version).await {
Ok(Some(manifest))
} else {
fetch_manifest_from_disk(&version).await.map(Some)
}

}
}
}

async fn fetch_manifest_from_disk(version: &Version) -> Result<ManifestKind> {
let mut file = File::open(format!("data/{}/manifest.json", version.version)).await
.context("Failed to load manifest.json")?;
if let Some(version) = versions.into_iter().find(|entry| entry.version == version) {
let disk_path = format!("data/{}/manifest.json", version.version);

let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await
.context("Failed to read manifest.json")?;
let manifest: ManifestKind = serde_json::from_slice(&buffer)
.context("Failed to parse manifest.json")?;
return match fetch_manifest_from_github(&version).await {
Ok(manifest) => {
if !StdPath::new(&disk_path).exists() {
write_to_json_file(&disk_path, &manifest).await?;
}
Ok(Some(manifest))
},
Err(_) => read_from_json_file(&disk_path).await.map(Some),
};
}

Ok(manifest)
Ok(None)
}

async fn fetch_manifest_from_github(version: &Version) -> Result<ManifestKind> {
let client = Client::new();
let response = client.get(&version.manifest).send().await
.context("Failed to send request to Github")?;

let manifest: ManifestKind = response.json().await
.context("Failed to parse response from Github")?;

write_manifest_to_disk(&manifest, version).await?;
let response = client.get(&version.manifest).send().await?;
let manifest: ManifestKind = response.json().await?;

Ok(manifest)
}

async fn write_manifest_to_disk(manifest: &ManifestKind, version: &Version) -> io::Result<()> {
let data = serde_json::to_string(manifest)?;
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(format!("data/{}/manifest.json", version.version))
.await?;

file.write_all(data.as_bytes()).await
}
Loading

0 comments on commit b9af0b3

Please sign in to comment.