feat: better api docs (#6586)

* feat: docs

* fix tombi

* chore: fix some of the routes rendering with missing /

* response schemas

* fix: restore labrinth docs routes

* Fix path parameter docs in routes

* remove utoipa-actix-web

* consistency

* improve version intros

* improve formatting, examples

* better hash examples, move openapi stuff to openapi.rs

* more utoipa param fixes

* request body docs

* chore: remove moderation route from v2, remove ingest & webhooks from v3 spec

* fixes

* chore: tweak sources titles

* fix

* fix test

* improve examples

* increase compiler spawned thread stack size

* remove unused tests & script

* test

* bro what

* fix

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
This commit is contained in:
François-Xavier Talbot
2026-07-04 14:19:21 +00:00
committed by GitHub
co-authored by aecsocket
parent b26d048a63
commit 4a6fa9fc3d
103 changed files with 4546 additions and 1387 deletions
+268 -26
View File
@@ -10,32 +10,56 @@ use crate::models::projects::{ProjectStatus, VersionStatus, VersionType};
use crate::models::teams::ProjectPermissions;
use crate::queue::session::AuthQueue;
use crate::routes::internal::delphi;
use crate::routes::{FileHash, HashAlgorithm};
use crate::{database, models};
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
use dashmap::DashMap;
use futures::TryStreamExt;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("version_file")
.route("{version_id}", web::get().to(get_version_from_hash))
.route("{version_id}/update", web::post().to(get_update_from_hash))
.route("project", web::post().to(get_projects_from_hashes))
.route("{version_id}", web::delete().to(delete_file))
.route("{version_id}/download", web::get().to(download_version)),
);
cfg.service(
web::scope("version_files")
// DEPRECATED - use `update_many` instead
// see `fn update_files` comment
.route("update", web::post().to(update_files))
.route("update_many", web::post().to(update_files_many))
.route("update_individual", web::post().to(update_individual_files))
.route("", web::post().to(get_versions_from_hashes)),
);
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_version_from_hash_route)
.service(get_update_from_hash_route)
.service(get_projects_from_hashes_route)
.service(delete_file_route)
.service(download_version_route)
.service(update_files_route)
.service(update_files_many_route)
.service(update_individual_files_route)
.service(get_versions_from_hashes_route);
}
/// Get version metadata by file hash.
#[utoipa::path(
tag = "version files",
get,
operation_id = "v3VersionFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
responses(
(status = 200, description = "Expected response to a valid request", body = models::projects::Version),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/version_file/{version_id}")]
pub async fn get_version_from_hash_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_version_from_hash(req, info, pool, redis, hash_query, session_queue)
.await
}
pub async fn get_version_from_hash(
@@ -89,7 +113,7 @@ pub async fn get_version_from_hash(
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct HashQuery {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub version_id: Option<VersionId>,
@@ -110,7 +134,7 @@ pub fn default_algorithm_from_hashes(hashes: &[String]) -> String {
"sha1".into()
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct UpdateData {
pub loaders: Option<Vec<String>>,
pub version_types: Option<Vec<VersionType>>,
@@ -123,6 +147,47 @@ pub struct UpdateData {
pub loader_fields: Option<HashMap<String, Vec<serde_json::Value>>>,
}
/// Get the latest matching version by file hash.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
request_body = UpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = models::projects::Version),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/version_file/{version_id}/update")]
pub async fn get_update_from_hash_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
update_data: web::Json<UpdateData>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_update_from_hash(
req,
info,
pool,
redis,
hash_query,
update_data,
session_queue,
)
.await
}
pub async fn get_update_from_hash(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -208,12 +273,40 @@ pub async fn get_update_from_hash(
}
// Requests above with multiple versions below
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct FileHashes {
/// Hash algorithm to use (sha1 or sha512)
#[schema(value_type = Option<HashAlgorithm>)]
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
#[schema(value_type = Vec<FileHash>)]
pub hashes: Vec<String>,
}
/// Get versions by file hashes.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3VersionsFromHashes",
request_body = FileHashes,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Version>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/version_files")]
pub async fn get_versions_from_hashes_route(
req: HttpRequest,
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
file_data: web::Json<FileHashes>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_versions_from_hashes(req, pool, redis, file_data, session_queue).await
}
pub async fn get_versions_from_hashes(
req: HttpRequest,
pool: web::Data<ReadOnlyPgPool>,
@@ -269,6 +362,31 @@ pub async fn get_versions_from_hashes(
Ok(HttpResponse::Ok().json(response))
}
/// Get projects by file hashes.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3ProjectsFromHashes",
request_body = FileHashes,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Project>),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[post("/version_file/project")]
pub async fn get_projects_from_hashes_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_data: web::Json<FileHashes>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
get_projects_from_hashes(req, pool, redis, file_data, session_queue).await
}
pub async fn get_projects_from_hashes(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -327,7 +445,7 @@ pub async fn get_projects_from_hashes(
Ok(HttpResponse::Ok().json(response))
}
#[derive(Deserialize)]
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ManyUpdateData {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>,
@@ -336,6 +454,26 @@ pub struct ManyUpdateData {
pub version_types: Option<Vec<VersionType>>,
}
/// Get latest matching versions by file hashes.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateFilesMany",
request_body = ManyUpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, Vec<models::projects::Version>>)
)
)]
#[post("/version_files/update_many")]
pub async fn update_files_many_route(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
update_data: web::Json<ManyUpdateData>,
) -> Result<web::Json<HashMap<String, Vec<models::projects::Version>>>, ApiError>
{
update_files_many(pool, redis, update_data).await
}
pub async fn update_files_many(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
@@ -368,6 +506,25 @@ pub async fn update_files_many(
// This endpoint is kept for backwards compat, since it still works in 99% of
// cases where H only maps to a single version, and for older clients. This
// endpoint will only take the first version for each file hash.
/// Get the latest matching version by file hash.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateFiles",
request_body = ManyUpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Version>)
)
)]
#[post("/version_files/update")]
pub async fn update_files_route(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
update_data: web::Json<ManyUpdateData>,
) -> Result<web::Json<HashMap<String, models::projects::Version>>, ApiError> {
update_files(pool, redis, update_data).await
}
pub async fn update_files(
pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
@@ -468,7 +625,7 @@ async fn update_files_internal(
Ok(response)
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct FileUpdateData {
pub hash: String,
pub loaders: Option<Vec<String>>,
@@ -476,12 +633,33 @@ pub struct FileUpdateData {
pub version_types: Option<Vec<VersionType>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct ManyFileUpdateData {
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<FileUpdateData>,
}
/// Get latest matching versions by individual file filters.
#[utoipa::path(
tag = "version files",
post,
operation_id = "v3UpdateIndividualFiles",
request_body = ManyFileUpdateData,
responses(
(status = 200, description = "Expected response to a valid request", body = HashMap<String, models::projects::Version>)
)
)]
#[post("/version_files/update_individual")]
pub async fn update_individual_files_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
update_data: web::Json<ManyFileUpdateData>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
update_individual_files(req, pool, redis, update_data, session_queue).await
}
pub async fn update_individual_files(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -603,6 +781,40 @@ pub async fn update_individual_files(
}
// under /api/v1/version_file/{hash}
/// Delete a file by hash.
#[utoipa::path(
tag = "version files",
delete,
operation_id = "v3DeleteFileFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID to delete from")
),
responses(
(status = NO_CONTENT, description = "Expected response to a valid request"),
(
status = 401,
description = "Incorrect token scopes or no authorization to access the requested item(s)"
),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[delete("/version_file/{version_id}")]
pub async fn delete_file_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
delete_file(req, info, pool, redis, hash_query, session_queue).await
}
pub async fn delete_file(
req: HttpRequest,
info: web::Path<(String,)>,
@@ -740,12 +952,42 @@ pub async fn delete_file(
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct DownloadRedirect {
pub url: String,
}
// under /api/v1/version_file/{hash}/download
/// Download a file by hash.
#[utoipa::path(
tag = "version files",
get,
operation_id = "v3DownloadVersionFromHash",
params(
("version_id" = String, Path, description = "The hexadecimal file hash"),
("algorithm" = Option<String>, Query, description = "Hash algorithm to use (sha1 or sha512)"),
("version_id" = Option<VersionId>, Query, description = "Optional version ID when hash maps to multiple files")
),
responses(
(status = 302, description = "Temporary redirect to file URL", body = DownloadRedirect),
(
status = 404,
description = "The requested item(s) were not found or no authorization to access the requested item(s)"
)
)
)]
#[get("/version_file/{version_id}/download")]
pub async fn download_version_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
download_version(req, info, pool, redis, hash_query, session_queue).await
}
pub async fn download_version(
req: HttpRequest,
info: web::Path<(String,)>,