You've already forked AstralRinth
forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
310
apps/labrinth/src/routes/v2_reroute.rs
Normal file
310
apps/labrinth/src/routes/v2_reroute.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::v3::project_creation::CreateError;
|
||||
use super::ApiError;
|
||||
use crate::models::v2::projects::LegacySideType;
|
||||
use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::header::{ContentDisposition, HeaderMap, TryIntoHeaderPair};
|
||||
use actix_web::HttpResponse;
|
||||
use futures::{stream, Future, StreamExt};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn extract_ok_json<T>(response: HttpResponse) -> Result<T, HttpResponse>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
// If the response is StatusCode::OK, parse the json and return it
|
||||
if response.status() == actix_web::http::StatusCode::OK {
|
||||
let failure_http_response = || {
|
||||
HttpResponse::InternalServerError().json(json!({
|
||||
"error": "reroute_error",
|
||||
"description": "Could not parse response from V2 redirection of route."
|
||||
}))
|
||||
};
|
||||
// Takes json out of HttpResponse, mutates it, then regenerates the HttpResponse
|
||||
let body = response.into_body();
|
||||
let bytes = actix_web::body::to_bytes(body)
|
||||
.await
|
||||
.map_err(|_| failure_http_response())?;
|
||||
let json_value: T = serde_json::from_slice(&bytes).map_err(|_| failure_http_response())?;
|
||||
Ok(json_value)
|
||||
} else {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
|
||||
// This only removes the body of 404 responses
|
||||
// This should not be used on the fallback no-route-found handler
|
||||
pub fn flatten_404_error(res: ApiError) -> Result<HttpResponse, ApiError> {
|
||||
match res {
|
||||
ApiError::NotFound => Ok(HttpResponse::NotFound().body("")),
|
||||
_ => Err(res),
|
||||
}
|
||||
}
|
||||
|
||||
// Allows internal modification of an actix multipart file
|
||||
// Expected:
|
||||
// 1. A json segment
|
||||
// 2. Any number of other binary segments
|
||||
// 'closure' is called with the json value, and the content disposition of the other segments
|
||||
pub async fn alter_actix_multipart<T, U, Fut>(
|
||||
mut multipart: Multipart,
|
||||
mut headers: HeaderMap,
|
||||
mut closure: impl FnMut(T, Vec<ContentDisposition>) -> Fut,
|
||||
) -> Result<Multipart, CreateError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
U: serde::Serialize,
|
||||
Fut: Future<Output = Result<U, CreateError>>,
|
||||
{
|
||||
let mut segments: Vec<MultipartSegment> = Vec::new();
|
||||
|
||||
let mut json = None;
|
||||
let mut json_segment = None;
|
||||
let mut content_dispositions = Vec::new();
|
||||
|
||||
if let Some(field) = multipart.next().await {
|
||||
let mut field = field?;
|
||||
let content_disposition = field.content_disposition().clone();
|
||||
let field_name = content_disposition.get_name().unwrap_or("");
|
||||
let field_filename = content_disposition.get_filename();
|
||||
let field_content_type = field.content_type();
|
||||
let field_content_type = field_content_type.map(|ct| ct.to_string());
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk?;
|
||||
buffer.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
{
|
||||
let json_value: T = serde_json::from_slice(&buffer)?;
|
||||
json = Some(json_value);
|
||||
}
|
||||
|
||||
json_segment = Some(MultipartSegment {
|
||||
name: field_name.to_string(),
|
||||
filename: field_filename.map(|s| s.to_string()),
|
||||
content_type: field_content_type,
|
||||
data: MultipartSegmentData::Binary(vec![]), // Initialize to empty, will be finished after
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(field) = multipart.next().await {
|
||||
let mut field = field?;
|
||||
let content_disposition = field.content_disposition().clone();
|
||||
let field_name = content_disposition.get_name().unwrap_or("");
|
||||
let field_filename = content_disposition.get_filename();
|
||||
let field_content_type = field.content_type();
|
||||
let field_content_type = field_content_type.map(|ct| ct.to_string());
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk?;
|
||||
buffer.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
content_dispositions.push(content_disposition.clone());
|
||||
segments.push(MultipartSegment {
|
||||
name: field_name.to_string(),
|
||||
filename: field_filename.map(|s| s.to_string()),
|
||||
content_type: field_content_type,
|
||||
data: MultipartSegmentData::Binary(buffer),
|
||||
})
|
||||
}
|
||||
|
||||
// Finishes the json segment, with aggregated content dispositions
|
||||
{
|
||||
let json_value = json.ok_or(CreateError::InvalidInput(
|
||||
"No json segment found in multipart.".to_string(),
|
||||
))?;
|
||||
let mut json_segment = json_segment.ok_or(CreateError::InvalidInput(
|
||||
"No json segment found in multipart.".to_string(),
|
||||
))?;
|
||||
|
||||
// Call closure, with the json value and names of the other segments
|
||||
let json_value: U = closure(json_value, content_dispositions).await?;
|
||||
let buffer = serde_json::to_vec(&json_value)?;
|
||||
json_segment.data = MultipartSegmentData::Binary(buffer);
|
||||
|
||||
// Insert the json segment at the beginning
|
||||
segments.insert(0, json_segment);
|
||||
}
|
||||
|
||||
let (boundary, payload) = generate_multipart(segments);
|
||||
|
||||
match (
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={}", boundary).as_str(),
|
||||
)
|
||||
.try_into_pair()
|
||||
{
|
||||
Ok((key, value)) => {
|
||||
headers.insert(key, value);
|
||||
}
|
||||
Err(err) => {
|
||||
CreateError::InvalidInput(format!("Error inserting test header: {:?}.", err));
|
||||
}
|
||||
};
|
||||
|
||||
let new_multipart = Multipart::new(&headers, stream::once(async { Ok(payload) }));
|
||||
|
||||
Ok(new_multipart)
|
||||
}
|
||||
|
||||
// Converts a "client_side" and "server_side" pair into the new v3 corresponding fields
|
||||
pub fn convert_side_types_v3(
|
||||
client_side: LegacySideType,
|
||||
server_side: LegacySideType,
|
||||
) -> HashMap<String, Value> {
|
||||
use LegacySideType::{Optional, Required};
|
||||
|
||||
let singleplayer = client_side == Required
|
||||
|| client_side == Optional
|
||||
|| server_side == Required
|
||||
|| server_side == Optional;
|
||||
let client_and_server = singleplayer;
|
||||
let client_only =
|
||||
(client_side == Required || client_side == Optional) && server_side != Required;
|
||||
let server_only =
|
||||
(server_side == Required || server_side == Optional) && client_side != Required;
|
||||
|
||||
let mut fields = HashMap::new();
|
||||
fields.insert("singleplayer".to_string(), json!(singleplayer));
|
||||
fields.insert("client_and_server".to_string(), json!(client_and_server));
|
||||
fields.insert("client_only".to_string(), json!(client_only));
|
||||
fields.insert("server_only".to_string(), json!(server_only));
|
||||
fields
|
||||
}
|
||||
|
||||
// Converts plugin loaders from v2 to v3, for search facets
|
||||
// Within every 1st and 2nd level (the ones allowed in v2), we convert every instance of:
|
||||
// "project_type:mod" to "project_type:plugin" OR "project_type:mod"
|
||||
pub fn convert_plugin_loader_facets_v3(facets: Vec<Vec<String>>) -> Vec<Vec<String>> {
|
||||
facets
|
||||
.into_iter()
|
||||
.map(|inner_facets| {
|
||||
if inner_facets == ["project_type:mod"] {
|
||||
vec![
|
||||
"project_type:plugin".to_string(),
|
||||
"project_type:datapack".to_string(),
|
||||
"project_type:mod".to_string(),
|
||||
]
|
||||
} else {
|
||||
inner_facets
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
// Convert search facets from V3 back to v2
|
||||
// this is not lossless. (See tests)
|
||||
pub fn convert_side_types_v2(
|
||||
side_types: &HashMap<String, Value>,
|
||||
project_type: Option<&str>,
|
||||
) -> (LegacySideType, LegacySideType) {
|
||||
let client_and_server = side_types
|
||||
.get("client_and_server")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
let singleplayer = side_types
|
||||
.get("singleplayer")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(client_and_server);
|
||||
let client_only = side_types
|
||||
.get("client_only")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
let server_only = side_types
|
||||
.get("server_only")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
convert_side_types_v2_bools(
|
||||
Some(singleplayer),
|
||||
client_only,
|
||||
server_only,
|
||||
Some(client_and_server),
|
||||
project_type,
|
||||
)
|
||||
}
|
||||
|
||||
// Client side, server side
|
||||
pub fn convert_side_types_v2_bools(
|
||||
singleplayer: Option<bool>,
|
||||
client_only: bool,
|
||||
server_only: bool,
|
||||
client_and_server: Option<bool>,
|
||||
project_type: Option<&str>,
|
||||
) -> (LegacySideType, LegacySideType) {
|
||||
use LegacySideType::{Optional, Required, Unknown, Unsupported};
|
||||
|
||||
match project_type {
|
||||
Some("plugin") => (Unsupported, Required),
|
||||
Some("datapack") => (Optional, Required),
|
||||
Some("shader") => (Required, Unsupported),
|
||||
Some("resourcepack") => (Required, Unsupported),
|
||||
_ => {
|
||||
let singleplayer = singleplayer.or(client_and_server).unwrap_or(false);
|
||||
|
||||
match (singleplayer, client_only, server_only) {
|
||||
// Only singleplayer
|
||||
(true, false, false) => (Required, Required),
|
||||
|
||||
// Client only and not server only
|
||||
(false, true, false) => (Required, Unsupported),
|
||||
(true, true, false) => (Required, Unsupported),
|
||||
|
||||
// Server only and not client only
|
||||
(false, false, true) => (Unsupported, Required),
|
||||
(true, false, true) => (Unsupported, Required),
|
||||
|
||||
// Both server only and client only
|
||||
(true, true, true) => (Optional, Optional),
|
||||
(false, true, true) => (Optional, Optional),
|
||||
|
||||
// Bad type
|
||||
(false, false, false) => (Unknown, Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capitalize_first(input: &str) -> String {
|
||||
let mut result = input.to_owned();
|
||||
if let Some(first_char) = result.get_mut(0..1) {
|
||||
first_char.make_ascii_uppercase();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::v2::projects::LegacySideType::{Optional, Required, Unsupported};
|
||||
|
||||
#[test]
|
||||
fn convert_types() {
|
||||
// Converting types from V2 to V3 and back should be idempotent- for certain pairs
|
||||
let lossy_pairs = [
|
||||
(Optional, Unsupported),
|
||||
(Unsupported, Optional),
|
||||
(Required, Optional),
|
||||
(Optional, Required),
|
||||
(Unsupported, Unsupported),
|
||||
];
|
||||
|
||||
for client_side in [Required, Optional, Unsupported] {
|
||||
for server_side in [Required, Optional, Unsupported] {
|
||||
if lossy_pairs.contains(&(client_side, server_side)) {
|
||||
continue;
|
||||
}
|
||||
let side_types = convert_side_types_v3(client_side, server_side);
|
||||
let (client_side2, server_side2) = convert_side_types_v2(&side_types, None);
|
||||
assert_eq!(client_side, client_side2);
|
||||
assert_eq!(server_side, server_side2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user