Rustic cleanups, dedups and making the code less hard to read in general (#251)

* typos :help_me:

* (part 1/?) massive cleanup to make the code more Rust-ic and cut down heap allocations.

* (part 2/?) massive cleanup to make the code more Rust-ic and cut down heap allocations.

* (part 3/?) cut down some pretty major heap allocations here - more Bytes and BytesMuts, less Vec<u8>s

also I don't really understand why you need to `to_vec` when you don't really use it again afterwards

* (part 4/?) deduplicate error handling in backblaze logic

* (part 5/?) fixes, cleanups, refactors, and reformatting

* (part 6/?) cleanups and refactors

* remove loads of `as_str` in types that already are `Display`

* Revert "remove loads of `as_str` in types that already are `Display`"

This reverts commit 4f974310cfb167ceba03001d81388db4f0fbb509.

* reformat and move routes util to the util module

* use streams

* Run prepare + formatting issues

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Leo Chen
2021-10-12 11:26:59 +08:00
committed by GitHub
parent 0010119440
commit 13187de97d
53 changed files with 997 additions and 1129 deletions

View File

@@ -1,9 +1,9 @@
use crate::health::status::test_database;
use crate::health::SEARCH_READY;
use actix_web::web::Data;
use actix_web::{get, HttpResponse};
use serde_json::json;
use crate::health::status::test_database;
use actix_web::web::Data;
use sqlx::PgPool;
use crate::health::SEARCH_READY;
use std::sync::atomic::Ordering;
#[get("/health")]
@@ -15,17 +15,17 @@ pub async fn health_get(client: Data<PgPool>) -> HttpResponse {
"ready": false,
"reason": "Database connection error"
});
return HttpResponse::InternalServerError().json(data)
return HttpResponse::InternalServerError().json(data);
}
if !SEARCH_READY.load(Ordering::Acquire) {
let data = json!({
"ready": false,
"reason": "Indexing is not finished"
});
return HttpResponse::InternalServerError().json(data)
return HttpResponse::InternalServerError().json(data);
}
HttpResponse::Ok().json(json!({
"ready": true,
"reason": "Everything is OK"
}))
}
}

View File

@@ -1,15 +1,14 @@
use actix_web::web;
mod v1;
pub use v1::v1_config;
mod auth;
mod health;
mod index;
mod maven;
mod moderation;
mod not_found;
mod notifications;
mod project_creation;
pub(crate) mod project_creation;
mod projects;
mod reports;
mod tags;
@@ -18,15 +17,15 @@ mod users;
mod version_creation;
mod version_file;
mod versions;
mod health;
pub use auth::config as auth_config;
pub use tags::config as tags_config;
pub use self::index::index_get;
pub use self::health::health_get;
pub use self::index::index_get;
pub use self::not_found::not_found;
use crate::file_hosting::FileHostingError;
use actix_web::web;
pub fn v2_config(cfg: &mut web::ServiceConfig) {
cfg.service(

View File

@@ -1,6 +1,6 @@
use super::ApiError;
use crate::database;
use crate::models::projects::{Project, ProjectStatus};
use crate::models::projects::ProjectStatus;
use crate::util::auth::check_is_moderator_from_headers;
use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::Deserialize;
@@ -43,10 +43,10 @@ pub async fn get_projects(
.try_collect::<Vec<database::models::ProjectId>>()
.await?;
let projects: Vec<Project> = database::Project::get_many_full(project_ids, &**pool)
let projects: Vec<_> = database::Project::get_many_full(project_ids, &**pool)
.await?
.into_iter()
.map(super::projects::convert_project)
.map(crate::models::projects::Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))

View File

@@ -1,6 +1,6 @@
use crate::database;
use crate::models::ids::NotificationId;
use crate::models::notifications::{Notification, NotificationAction};
use crate::models::notifications::Notification;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
@@ -20,22 +20,25 @@ pub async fn notifications_get(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&*ids.ids)?
.into_iter()
.map(|x| x.into())
.collect();
// TODO: this is really confusingly named.
use database::models::notification_item::Notification as DBNotification;
use database::models::NotificationId as DBNotificationId;
let notifications_data =
let notification_ids: Vec<DBNotificationId> =
serde_json::from_str::<Vec<NotificationId>>(ids.ids.as_str())?
.into_iter()
.map(DBNotificationId::from)
.collect();
let notifications_data: Vec<DBNotification> =
database::models::notification_item::Notification::get_many(notification_ids, &**pool)
.await?;
let mut notifications: Vec<Notification> = Vec::new();
for notification in notifications_data {
if notification.user_id == user.id.into() || user.role.is_mod() {
notifications.push(convert_notification(notification));
}
}
let notifications: Vec<Notification> = notifications_data
.into_iter()
.filter(|n| n.user_id == user.id.into() || user.role.is_mod())
.map(Notification::from)
.collect();
Ok(HttpResponse::Ok().json(notifications))
}
@@ -55,7 +58,7 @@ pub async fn notification_get(
if let Some(data) = notification_data {
if user.id == data.user_id.into() || user.role.is_mod() {
Ok(HttpResponse::Ok().json(convert_notification(data)))
Ok(HttpResponse::Ok().json(Notification::from(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
}
@@ -64,29 +67,6 @@ pub async fn notification_get(
}
}
pub fn convert_notification(
notif: database::models::notification_item::Notification,
) -> Notification {
Notification {
id: notif.id.into(),
user_id: notif.user_id.into(),
type_: notif.notification_type,
title: notif.title,
text: notif.text,
link: notif.link,
read: notif.read,
created: notif.created,
actions: notif
.actions
.into_iter()
.map(|x| NotificationAction {
title: x.title,
action_route: (x.action_route_method, x.action_route),
})
.collect(),
}
}
#[delete("{id}")]
pub async fn notification_delete(
req: HttpRequest,

View File

@@ -8,6 +8,7 @@ use crate::models::users::UserId;
use crate::routes::version_creation::InitialVersionData;
use crate::search::indexing::IndexingError;
use crate::util::auth::{get_user_from_headers, AuthenticationError};
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use actix_multipart::{Field, Multipart};
use actix_web::http::StatusCode;
@@ -255,7 +256,6 @@ pub async fn project_create(
result
}
/*
Project Creation Steps:
@@ -449,18 +449,12 @@ pub async fn project_create_inner(
}
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
let mut data = Vec::new();
while let Some(chunk) = field.next().await {
const FILE_SIZE_CAP: usize = 5 * (1 << 20);
if data.len() >= FILE_SIZE_CAP {
return Err(CreateError::InvalidInput(String::from(
"Gallery image exceeds the maximum of 5MiB.",
)));
} else {
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
}
}
let data = read_from_field(
&mut field,
5 * (1 << 20),
"Gallery image exceeds the maximum of 5MiB.",
)
.await?;
let hash = sha1::Sha1::from(&data).hexdigest();
let (_, file_extension) =
@@ -470,7 +464,7 @@ pub async fn project_create_inner(
let url = format!("data/{}/images/{}.{}", project_id, hash, file_extension);
let upload_data = file_host
.upload_file(content_type, &url, data.to_vec())
.upload_file(content_type, &url, data.freeze())
.await?;
uploaded_files.push(UploadedFile {
@@ -804,22 +798,13 @@ async fn process_icon_upload(
cdn_url: &str,
) -> Result<String, CreateError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
let mut data = Vec::new();
while let Some(chunk) = field.next().await {
if data.len() >= 262144 {
return Err(CreateError::InvalidInput(String::from(
"Icons must be smaller than 256KiB",
)));
} else {
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
}
}
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/icon.{}", project_id, file_extension),
data,
data.freeze(),
)
.await?;

View File

@@ -2,14 +2,14 @@ use crate::database;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::projects::{
DonationLink, GalleryItem, License, ModeratorMessage, ProjectId, ProjectStatus, SearchRequest,
SideType,
DonationLink, Project, ProjectId, ProjectStatus, SearchRequest, SideType,
};
use crate::models::teams::Permissions;
use crate::routes::ApiError;
use crate::search::indexing::queue::CreationQueue;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::get_user_from_headers;
use crate::util::auth::{get_user_from_headers, is_authorized};
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::web::Data;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -48,36 +48,16 @@ pub async fn projects_get(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let mut projects = Vec::new();
for project_data in projects_data {
let mut authorized = !project_data.status.is_hidden();
if let Some(user) = &user_option {
if !authorized {
if user.role.is_mod() {
authorized = true;
} else {
let user_id: database::models::ids::UserId = user.id.into();
let project_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
project_data.inner.team_id as database::models::ids::TeamId,
user_id as database::models::ids::UserId,
)
.fetch_one(&**pool)
.await?
.exists;
authorized = project_exists.unwrap_or(false);
}
let projects: Vec<_> = futures::stream::iter(projects_data)
.filter_map(|data| async {
if is_authorized(&data, &user_option, &pool).await.ok()? {
Some(Project::from(data))
} else {
None
}
}
if authorized {
projects.push(convert_project(project_data));
}
}
})
.collect()
.await;
Ok(HttpResponse::Ok().json(projects))
}
@@ -97,37 +77,11 @@ pub async fn project_get(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(data) = project_data {
let mut authorized = !data.status.is_hidden();
if let Some(user) = user_option {
if !authorized {
if user.role.is_mod() {
authorized = true;
} else {
let user_id: database::models::ids::UserId = user.id.into();
let project_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
data.inner.team_id as database::models::ids::TeamId,
user_id as database::models::ids::UserId,
)
.fetch_one(&**pool)
.await?
.exists;
authorized = project_exists.unwrap_or(false);
}
}
if is_authorized(&data, &user_option, &pool).await? {
return Ok(HttpResponse::Ok().json(Project::from(data)));
}
if authorized {
return Ok(HttpResponse::Ok().json(convert_project(data)));
}
Ok(HttpResponse::NotFound().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
Ok(HttpResponse::NotFound().body(""))
}
#[derive(Serialize)]
@@ -189,12 +143,12 @@ pub async fn dependency_list(
let projects = projects_result?
.into_iter()
.map(convert_project)
.collect::<Vec<models::projects::Project>>();
.map(models::projects::Project::from)
.collect::<Vec<_>>();
let versions = versions_result?
.into_iter()
.map(super::versions::convert_version)
.collect::<Vec<models::projects::Version>>();
.map(models::projects::Version::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(DependencyInfo { projects, versions }))
} else {
@@ -202,71 +156,6 @@ pub async fn dependency_list(
}
}
pub fn convert_project(
data: database::models::project_item::QueryProject,
) -> models::projects::Project {
let m = data.inner;
models::projects::Project {
id: m.id.into(),
slug: m.slug,
project_type: data.project_type,
team: m.team_id.into(),
title: m.title,
description: m.description,
body: m.body,
body_url: m.body_url,
published: m.published,
updated: m.updated,
status: data.status,
moderator_message: if let Some(message) = m.moderation_message {
Some(ModeratorMessage {
message,
body: m.moderation_message_body,
})
} else {
None
},
license: License {
id: data.license_id,
name: data.license_name,
url: m.license_url,
},
client_side: data.client_side,
server_side: data.server_side,
downloads: m.downloads as u32,
followers: m.follows as u32,
categories: data.categories,
versions: data.versions.into_iter().map(|v| v.into()).collect(),
icon_url: m.icon_url,
issues_url: m.issues_url,
source_url: m.source_url,
wiki_url: m.wiki_url,
discord_url: m.discord_url,
donation_urls: Some(
data.donation_urls
.into_iter()
.map(|d| DonationLink {
id: d.platform_short,
platform: d.platform_name,
url: d.url,
})
.collect(),
),
gallery: data
.gallery_items
.into_iter()
.map(|x| GalleryItem {
url: x.image_url,
featured: x.featured,
title: x.title,
description: x.description,
created: x.created,
})
.collect(),
}
}
/// A project returned from the API
#[derive(Serialize, Deserialize, Validate)]
pub struct EditProject {
@@ -476,7 +365,7 @@ pub async fn project_edit(
if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(
convert_project(project_item.clone()),
Project::from(project_item.clone()),
webhook_url,
)
.await
@@ -959,30 +848,15 @@ pub async fn project_icon_edit(
}
}
let mut bytes = web::BytesMut::new();
while let Some(item) = payload.next().await {
if bytes.len() >= 262144 {
return Err(ApiError::InvalidInputError(String::from(
"Icons must be smaller than 256KiB",
)));
} else {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError(
"Unable to parse bytes in payload sent!".to_string(),
)
})?);
}
}
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let project_id: ProjectId = project_item.id.into();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/{}.{}", project_id, hash, ext.ext),
bytes.to_vec(),
bytes.freeze(),
)
.await?;
@@ -1126,29 +1000,18 @@ pub async fn add_gallery_item(
}
}
let mut bytes = web::BytesMut::new();
while let Some(item) = payload.next().await {
const FILE_SIZE_CAP: usize = 5 * (1 << 20);
if bytes.len() >= FILE_SIZE_CAP {
return Err(ApiError::InvalidInputError(String::from(
"Gallery image exceeds the maximum of 5MiB.",
)));
} else {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError(
"Unable to parse bytes in payload sent!".to_string(),
)
})?);
}
}
let bytes = read_from_payload(
&mut payload,
5 * (1 << 20),
"Gallery image exceeds the maximum of 5MiB.",
)
.await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let id: ProjectId = project_item.id.into();
let url = format!("data/{}/images/{}.{}", id, hash, &*ext.ext);
file_host
.upload_file(content_type, &url, bytes.to_vec())
.upload_file(content_type, &url, bytes.freeze())
.await?;
let mut transaction = pool.begin().await?;

View File

@@ -1,5 +1,4 @@
use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder};
use crate::database::models::team_item::QueryTeamMember;
use crate::database::models::TeamMember;
use crate::models::ids::ProjectId;
use crate::models::teams::{Permissions, TeamId};
@@ -32,19 +31,19 @@ pub async fn team_members_get_project(
.map_err(ApiError::DatabaseError)?;
if team_member.is_some() {
let team_members: Vec<crate::models::teams::TeamMember> = members_data
let team_members: Vec<_> = members_data
.into_iter()
.map(|data| convert_team_member(data, false))
.map(|data| crate::models::teams::TeamMember::from(data, false))
.collect();
return Ok(HttpResponse::Ok().json(team_members));
}
}
let team_members: Vec<crate::models::teams::TeamMember> = members_data
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| x.accepted)
.map(|data| convert_team_member(data, true))
.map(|data| crate::models::teams::TeamMember::from(data, true))
.collect();
Ok(HttpResponse::Ok().json(team_members))
@@ -53,23 +52,6 @@ pub async fn team_members_get_project(
}
}
pub fn convert_team_member(
data: QueryTeamMember,
override_permissions: bool,
) -> crate::models::teams::TeamMember {
crate::models::teams::TeamMember {
team_id: data.team_id.into(),
user: super::users::convert_user(data.user),
role: data.role,
permissions: if override_permissions {
None
} else {
Some(data.permissions)
},
accepted: data.accepted,
}
}
#[get("{id}/members")]
pub async fn team_members_get(
req: HttpRequest,
@@ -87,19 +69,19 @@ pub async fn team_members_get(
.map_err(ApiError::DatabaseError)?;
if team_member.is_some() {
let team_members: Vec<crate::models::teams::TeamMember> = members_data
let team_members: Vec<_> = members_data
.into_iter()
.map(|data| convert_team_member(data, false))
.map(|data| crate::models::teams::TeamMember::from(data, false))
.collect();
return Ok(HttpResponse::Ok().json(team_members));
}
}
let team_members: Vec<crate::models::teams::TeamMember> = members_data
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| x.accepted)
.map(|data| convert_team_member(data, true))
.map(|data| crate::models::teams::TeamMember::from(data, true))
.collect();
Ok(HttpResponse::Ok().json(team_members))

View File

@@ -3,12 +3,11 @@ use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
use crate::models::projects::{Project, ProjectStatus};
use crate::models::users::{Role, UserId};
use crate::routes::notifications::convert_notification;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
use futures::StreamExt;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
@@ -42,7 +41,7 @@ pub async fn users_get(
let users_data = User::get_many(user_ids, &**pool).await?;
let users: Vec<crate::models::users::User> = users_data.into_iter().map(convert_user).collect();
let users: Vec<crate::models::users::User> = users_data.into_iter().map(From::from).collect();
Ok(HttpResponse::Ok().json(users))
}
@@ -68,27 +67,13 @@ pub async fn user_get(
}
if let Some(data) = user_data {
let response = convert_user(data);
let response: crate::models::users::User = data.into();
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
pub fn convert_user(data: crate::database::models::user_item::User) -> crate::models::users::User {
crate::models::users::User {
id: data.id.into(),
github_id: data.github_id.map(|i| i as u64),
username: data.username,
name: data.name,
email: None,
avatar_url: data.avatar_url,
bio: data.bio,
created: data.created,
role: Role::from_string(&*data.role),
}
}
#[get("{user_id}/projects")]
pub async fn projects_list(
req: HttpRequest,
@@ -114,11 +99,11 @@ pub async fn projects_list(
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await?
};
let response = crate::database::Project::get_many_full(project_data, &**pool)
let response: Vec<_> = crate::database::Project::get_many_full(project_data, &**pool)
.await?
.into_iter()
.map(super::projects::convert_project)
.collect::<Vec<Project>>();
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(response))
} else {
@@ -337,26 +322,15 @@ pub async fn user_icon_edit(
}
}
let mut bytes = web::BytesMut::new();
while let Some(item) = payload.next().await {
if bytes.len() >= 262144 {
return Err(ApiError::InvalidInputError(String::from(
"Icons must be smaller than 256KiB",
)));
} else {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError(
"Unable to parse bytes in payload sent!".to_string(),
)
})?);
}
}
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB")
.await?;
let upload_data = file_host
.upload_file(
content_type,
&format!("user/{}/icon.{}", user_id, ext.ext),
bytes.to_vec(),
bytes.freeze(),
)
.await?;
@@ -468,11 +442,11 @@ pub async fn user_follows(
.try_collect::<Vec<crate::database::models::ProjectId>>()
.await?;
let projects = crate::database::Project::get_many_full(project_ids, &**pool)
let projects: Vec<_> = crate::database::Project::get_many_full(project_ids, &**pool)
.await?
.into_iter()
.map(super::projects::convert_project)
.collect::<Vec<Project>>();
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))
} else {
@@ -502,7 +476,7 @@ pub async fn user_notifications(
crate::database::models::notification_item::Notification::get_many_user(id, &**pool)
.await?
.into_iter()
.map(convert_notification)
.map(Into::into)
.collect();
notifications.sort_by(|a, b| b.created.cmp(&a.created));

View File

@@ -34,10 +34,10 @@ pub async fn get_mods(
.try_collect::<Vec<database::models::ProjectId>>()
.await?;
let projects: Vec<Project> = database::Project::get_many_full(project_ids, &**pool)
let projects: Vec<_> = database::Project::get_many_full(project_ids, &**pool)
.await?
.into_iter()
.map(crate::routes::projects::convert_project)
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))

View File

@@ -1,10 +1,10 @@
use crate::file_hosting::FileHost;
use crate::models::projects::SearchRequest;
use crate::routes::project_creation::{project_create_inner, undo_uploads, CreateError};
use crate::routes::projects::{convert_project, ProjectIds};
use crate::routes::projects::ProjectIds;
use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::get_user_from_headers;
use crate::util::auth::{get_user_from_headers, is_authorized};
use crate::{database, models};
use actix_multipart::Multipart;
use actix_web::web;
@@ -98,37 +98,14 @@ pub async fn mods_get(
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let mut projects = Vec::new();
let mut projects = Vec::with_capacity(projects_data.len());
for project_data in projects_data {
let mut authorized = !project_data.status.is_hidden();
if let Some(user) = &user_option {
if !authorized {
if user.role.is_mod() {
authorized = true;
} else {
let user_id: database::models::ids::UserId = user.id.into();
let project_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
project_data.inner.team_id as database::models::ids::TeamId,
user_id as database::models::ids::UserId,
)
.fetch_one(&**pool)
.await?
.exists;
authorized = project_exists.unwrap_or(false);
}
}
}
if authorized {
projects.push(convert_project(project_data));
// can't use `map` and `collect` here since `is_authorized` must be async
for proj in projects_data {
if is_authorized(&proj, &user_option, &pool).await? {
projects.push(crate::models::projects::Project::from(proj))
}
}
Ok(HttpResponse::Ok().json(projects))
}

View File

@@ -2,7 +2,7 @@ use crate::file_hosting::FileHost;
use crate::models::ids::{ProjectId, UserId, VersionId};
use crate::models::projects::{Dependency, GameVersion, Loader, Version, VersionFile, VersionType};
use crate::models::teams::Permissions;
use crate::routes::versions::{convert_version, VersionIds, VersionListFilters};
use crate::routes::versions::{VersionIds, VersionListFilters};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::{database, models, Pepper};
@@ -91,7 +91,7 @@ pub async fn version_list(
.map(|featured| featured == version.featured)
.unwrap_or(true)
})
.map(convert_version)
.map(Version::from)
.map(convert_to_legacy)
.collect::<Vec<_>>();
@@ -118,16 +118,14 @@ pub async fn version_list(
version.game_versions.contains(&filter.0.version)
&& version.loaders.contains(&filter.1.loader)
})
.map(|version| {
response.push(convert_to_legacy(convert_version(version.clone())))
})
.map(|version| response.push(convert_to_legacy(Version::from(version.clone()))))
.unwrap_or(());
});
if response.is_empty() {
versions
.into_iter()
.for_each(|version| response.push(convert_to_legacy(convert_version(version))));
.for_each(|version| response.push(convert_to_legacy(Version::from(version))));
}
}
@@ -154,7 +152,7 @@ pub async fn versions_get(
let mut versions = Vec::new();
for version_data in versions_data {
versions.push(convert_to_legacy(convert_version(version_data)));
versions.push(convert_to_legacy(Version::from(version_data)));
}
Ok(HttpResponse::Ok().json(versions))
@@ -169,7 +167,7 @@ pub async fn version_get(
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
if let Some(data) = version_data {
Ok(HttpResponse::Ok().json(convert_to_legacy(convert_version(data))))
Ok(HttpResponse::Ok().json(convert_to_legacy(Version::from(data))))
} else {
Ok(HttpResponse::NotFound().body(""))
}
@@ -214,7 +212,7 @@ pub async fn get_version_from_hash(
.await?;
if let Some(data) = version_data {
Ok(HttpResponse::Ok().json(super::versions::convert_version(data)))
Ok(HttpResponse::Ok().json(crate::models::projects::Version::from(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
}

View File

@@ -8,6 +8,7 @@ use crate::models::projects::{
use crate::models::teams::Permissions;
use crate::routes::project_creation::{CreateError, UploadedFile};
use crate::util::auth::get_user_from_headers;
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use crate::validate::{validate_file, ValidationResult};
use actix_multipart::{Field, Multipart};
@@ -587,20 +588,10 @@ pub async fn upload_file(
let content_type = crate::util::ext::project_file_type(file_extension)
.ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?;
let mut data = Vec::new();
while let Some(chunk) = field.next().await {
// Project file size limit of 100MiB
const FILE_SIZE_CAP: usize = 100 * (1 << 20);
if data.len() >= FILE_SIZE_CAP {
return Err(CreateError::InvalidInput(
String::from("Project file exceeds the maximum of 100MiB. Contact a moderator or admin to request permission to upload larger files.")
));
} else {
let bytes = chunk.map_err(CreateError::MultipartError)?;
data.append(&mut bytes.to_vec());
}
}
let data = read_from_field(
field, 100 * (1 << 20),
"Project file exceeds the maximum of 100MiB. Contact a moderator or admin to request permission to upload larger files."
).await?;
let hash = sha1::Sha1::from(&data).hexdigest();
let exists = sqlx::query!(
@@ -623,7 +614,7 @@ pub async fn upload_file(
}
let validation_result = validate_file(
data.as_slice(),
&data,
file_extension,
project_type,
loaders,
@@ -638,7 +629,7 @@ pub async fn upload_file(
"data/{}/versions/{}/{}",
project_id, version_number, file_name
),
data.to_vec(),
data.freeze(),
)
.await?;

View File

@@ -1,9 +1,11 @@
use super::ApiError;
use crate::database::models::version_item::QueryVersion;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::projects::{GameVersion, Loader};
use crate::models::projects::{GameVersion, Loader, Version};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::util::routes::ok_or_not_found;
use crate::{database, Pepper};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
@@ -51,7 +53,7 @@ pub async fn get_version_from_hash(
.await?;
if let Some(data) = version_data {
Ok(HttpResponse::Ok().json(super::versions::convert_version(data)))
Ok(HttpResponse::Ok().json(models::projects::Version::from(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
}
@@ -361,11 +363,7 @@ pub async fn get_update_from_hash(
if let Some(version_id) = version_ids.last() {
let version_data = database::models::Version::get_full(*version_id, &**pool).await?;
if let Some(data) = version_data {
Ok(HttpResponse::Ok().json(super::versions::convert_version(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
}
ok_or_not_found::<QueryVersion, Version>(version_data)
} else {
Ok(HttpResponse::NotFound().body(""))
}
@@ -414,14 +412,16 @@ pub async fn get_versions_from_hashes(
)
.await?;
let mut response = HashMap::new();
for row in result {
if let Some(version) = versions_data.iter().find(|x| x.id.0 == row.version_id) {
response.insert(row.hash, super::versions::convert_version(version.clone()));
}
}
let response: Vec<_> = result
.into_iter()
.filter_map(|row| {
versions_data
.clone()
.into_iter()
.find(|x| x.id.0 == row.version_id)
.map(|v| (row.hash, crate::models::projects::Version::from(v)))
})
.collect();
Ok(HttpResponse::Ok().json(response))
}
@@ -542,7 +542,7 @@ pub async fn update_files(
if let Some(version) = versions.iter().find(|x| x.id.0 == row.version_id) {
response.insert(
row.hash.clone(),
super::versions::convert_version(version.clone()),
models::projects::Version::from(version.clone()),
);
}
}

View File

@@ -1,7 +1,7 @@
use super::ApiError;
use crate::database;
use crate::models;
use crate::models::projects::{Dependency, DependencyType};
use crate::models::projects::{Dependency, Version};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::util::validate::validation_errors_to_string;
@@ -55,7 +55,7 @@ pub async fn version_list(
.map(|featured| featured == version.featured)
.unwrap_or(true)
})
.map(convert_version)
.map(Version::from)
.collect::<Vec<_>>();
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
@@ -83,14 +83,14 @@ pub async fn version_list(
version.game_versions.contains(&filter.0.version)
&& version.loaders.contains(&filter.1.loader)
})
.map(|version| response.push(convert_version(version.clone())))
.map(|version| response.push(Version::from(version.clone())))
.unwrap_or(());
});
if response.is_empty() {
versions
.into_iter()
.for_each(|version| response.push(convert_version(version)));
.for_each(|version| response.push(Version::from(version)));
}
}
@@ -119,12 +119,10 @@ pub async fn versions_get(
.collect();
let versions_data = database::models::Version::get_many_full(version_ids, &**pool).await?;
let mut versions = Vec::new();
for version_data in versions_data {
versions.push(convert_version(version_data));
}
let versions = versions_data
.into_iter()
.map(Version::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(versions))
}
@@ -137,77 +135,12 @@ pub async fn version_get(
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
if let Some(data) = version_data {
Ok(HttpResponse::Ok().json(convert_version(data)))
Ok(HttpResponse::Ok().json(models::projects::Version::from(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
pub fn convert_version(
data: database::models::version_item::QueryVersion,
) -> models::projects::Version {
use models::projects::VersionType;
models::projects::Version {
id: data.id.into(),
project_id: data.project_id.into(),
author_id: data.author_id.into(),
featured: data.featured,
name: data.name,
version_number: data.version_number,
changelog: data.changelog,
changelog_url: data.changelog_url,
date_published: data.date_published,
downloads: data.downloads as u32,
version_type: match data.version_type.as_str() {
"release" => VersionType::Release,
"beta" => VersionType::Beta,
"alpha" => VersionType::Alpha,
_ => VersionType::Release,
},
files: data
.files
.into_iter()
.map(|f| {
models::projects::VersionFile {
url: f.url,
filename: f.filename,
// FIXME: Hashes are currently stored as an ascii byte slice instead
// of as an actual byte array in the database
hashes: f
.hashes
.into_iter()
.map(|(k, v)| Some((k, String::from_utf8(v).ok()?)))
.collect::<Option<_>>()
.unwrap_or_else(Default::default),
primary: f.primary,
}
})
.collect(),
dependencies: data
.dependencies
.into_iter()
.map(|d| Dependency {
version_id: d.version_id.map(|x| x.into()),
project_id: d.project_id.map(|x| x.into()),
dependency_type: DependencyType::from_str(&*d.dependency_type),
})
.collect(),
game_versions: data
.game_versions
.into_iter()
.map(models::projects::GameVersion)
.collect(),
loaders: data
.loaders
.into_iter()
.map(models::projects::Loader)
.collect(),
}
}
#[derive(Serialize, Deserialize, Validate)]
pub struct EditVersion {
#[validate(length(min = 3, max = 256))]