fix: attribution scanning details (#6651)

* key override file sources on file path, allow deleting groups

* dont scan modrinth files

* delete attr group button

* return what override files already exist on platform

* intl:extract

* uppercase queries

* fix dupe migration key
This commit is contained in:
aecsocket
2026-07-12 09:04:54 +00:00
committed by GitHub
parent 1741d12b7b
commit e1556a8ee3
15 changed files with 346 additions and 106 deletions
@@ -1,4 +1,4 @@
use actix_web::{HttpRequest, get, patch, post, web};
use actix_web::{HttpRequest, delete, get, patch, post, web};
use chrono::{DateTime, Utc};
use eyre::eyre;
use serde::{Deserialize, Serialize};
@@ -31,6 +31,7 @@ use crate::util::error::Context;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(list)
.service(update_group)
.service(delete_group)
.service(scan)
.service(force_scan_file)
.service(assign)
@@ -46,6 +47,7 @@ struct AttributionGroupResponse {
attributed_by: Option<ariadne::ids::UserId>,
files: Vec<AttributionFileResponse>,
versions: Vec<VersionInfo>,
override_files_on_platform: Vec<OverrideFileOnPlatformResponse>,
}
#[derive(Clone, Serialize, utoipa::ToSchema)]
@@ -67,6 +69,15 @@ struct AttributionFileResponse {
moderation_external_license: Option<ModerationExternalLicenseResponse>,
}
#[derive(Clone, Serialize, utoipa::ToSchema)]
struct OverrideFileOnPlatformResponse {
file_path: String,
sha1: String,
version_id: VersionId,
platform_version_id: VersionId,
platform_project_id: ProjectId,
}
#[derive(Clone, Serialize, utoipa::ToSchema)]
struct ModerationExternalLicenseResponse {
id: i64,
@@ -358,6 +369,48 @@ pub async fn list(
.wrap_internal_err("failed to fetch attribution group files")?
};
let override_files_on_platform = if requester_is_mod {
sqlx::query!(
r#"
SELECT
ofs.file_path AS "file_path!",
CONVERT_FROM(ofs.sha1, 'UTF8') AS "sha1!",
source_file.version_id AS "version_id: DBVersionId",
platform_file.version_id AS "platform_version_id: DBVersionId",
platform_version.mod_id AS "platform_project_id: DBProjectId"
FROM files source_file
INNER JOIN versions source_version
ON source_version.id = source_file.version_id
INNER JOIN override_file_sources ofs
ON ofs.file_id = source_file.id
INNER JOIN hashes h
ON h.algorithm = 'sha1' AND h.hash = ofs.sha1
INNER JOIN files platform_file
ON platform_file.id = h.file_id
INNER JOIN versions platform_version
ON platform_version.id = platform_file.version_id
WHERE source_version.mod_id = $1
"#,
project_id as DBProjectId,
)
.fetch_all(pool.as_ref())
.await
.wrap_internal_err(
"failed to fetch override files already on platform",
)?
.into_iter()
.map(|row| OverrideFileOnPlatformResponse {
file_path: row.file_path,
sha1: row.sha1,
version_id: row.version_id.into(),
platform_version_id: row.platform_version_id.into(),
platform_project_id: row.platform_project_id.into(),
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let moderation_external_licenses = if requester_is_mod {
let mut ids: Vec<i64> = files
.iter()
@@ -497,6 +550,15 @@ pub async fn list(
.filter(|version| group_version_ids.contains(&version.id))
.cloned()
.collect();
let group_file_sha1s = group_files
.iter()
.map(|file| file.sha1.as_str())
.collect::<std::collections::HashSet<_>>();
let group_override_files_on_platform = override_files_on_platform
.iter()
.filter(|file| group_file_sha1s.contains(file.sha1.as_str()))
.cloned()
.collect();
let mut attribution = group.attribution.and_then(|v| {
serde_json::from_value::<AttributionResolution>(v).ok()
@@ -530,6 +592,7 @@ pub async fn list(
attributed_by,
files: group_files,
versions: group_versions,
override_files_on_platform: group_override_files_on_platform,
});
}
@@ -620,6 +683,88 @@ pub async fn update_group(
Ok(())
}
/// Delete an attribution group and all files inside it.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
responses((status = NO_CONTENT))
)]
#[delete("/group/{group_id}")]
pub async fn delete_group(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<i64>,
) -> Result<(), ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await?;
let mut txn = pool.begin().await.wrap_internal_err(
"failed to begin attribution group deletion transaction",
)?;
let group_id = path.into_inner();
let version_ids = sqlx::query_scalar!(
r#"
SELECT DISTINCT f.version_id AS "version_id: DBVersionId"
FROM project_attribution_files paf
INNER JOIN project_attribution_groups pag ON pag.id = paf.group_id
INNER JOIN override_file_sources ofs ON ofs.sha1 = paf.sha1
INNER JOIN files f ON f.id = ofs.file_id
INNER JOIN versions v ON v.id = f.version_id
WHERE paf.group_id = $1
AND pag.project_id = v.mod_id
"#,
group_id,
)
.fetch_all(&mut txn)
.await
.wrap_internal_err("failed to fetch attribution group versions")?;
sqlx::query!(
"
DELETE FROM project_attribution_files
WHERE group_id = $1
",
group_id,
)
.execute(&mut txn)
.await
.wrap_internal_err("failed to delete attribution group files")?;
let result = sqlx::query!(
"
DELETE FROM project_attribution_groups
WHERE id = $1
",
group_id,
)
.execute(&mut txn)
.await
.wrap_internal_err("failed to delete attribution group")?;
if result.rows_affected() == 0 {
return Err(ApiError::NotFound);
}
txn.commit().await.wrap_internal_err(
"failed to commit attribution group deletion transaction",
)?;
DBVersion::clear_cache_ids(&version_ids, redis.as_ref())
.await
.wrap_internal_err("failed to clear version attribution cache")?;
Ok(())
}
#[derive(Deserialize, utoipa::ToSchema)]
struct AssignBody {
sha1: String,
+1
View File
@@ -142,6 +142,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
attribution::scan,
attribution::list,
attribution::update_group,
attribution::delete_group,
attribution::assign,
attribution::split,
billing::products,