Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/app-frontend/src/helpers/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,13 @@ export async function add_project_from_version(
path: string,
versionId: string,
reason: DownloadReason,
dependentOnVersionId?: string,
): Promise<string> {
return await invoke('plugin:profile|profile_add_project_from_version', {
path,
versionId,
reason,
dependentOnVersionId,
})
}

Expand Down
14 changes: 9 additions & 5 deletions apps/app-frontend/src/store/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const installVersionDependencies = async (profile, version, reason, onDep
return installed
}

const queueInstall = async (projectId, resolvedVersion) => {
const queueInstall = async (projectId, resolvedVersion, dependentOn) => {
if (!resolvedVersion?.id) return false

const versionId = resolvedVersion.id
Expand All @@ -91,7 +91,11 @@ export const installVersionDependencies = async (profile, version, reason, onDep
if (resolvedProjectId) {
queuedProjectVersions.set(resolvedProjectId, versionId)
}
queuedInstalls.push({ versionId, projectId: resolvedProjectId })
queuedInstalls.push({
versionId,
projectId: resolvedProjectId,
dependentOnVersionId: dependentOn?.id,
})
return true
}

Expand Down Expand Up @@ -159,7 +163,7 @@ export const installVersionDependencies = async (profile, version, reason, onDep
if (!resolved) continue

const { depVersion, depProjectId } = resolved
const queued = await queueInstall(depProjectId, depVersion)
const queued = await queueInstall(depProjectId, depVersion, inputVersion)
if (queued && depProjectId) {
await announceDependency(depProjectId, depVersion)
}
Expand All @@ -176,8 +180,8 @@ export const installVersionDependencies = async (profile, version, reason, onDep
for (let i = 0; i < queuedInstalls.length; i += batchSize) {
const batch = queuedInstalls.slice(i, i + batchSize)
await Promise.all(
batch.map(async ({ versionId }) => {
await add_project_from_version(profile.path, versionId, reason)
batch.map(async ({ versionId, dependentOnVersionId }) => {
await add_project_from_version(profile.path, versionId, reason, dependentOnVersionId)
}),
)
}
Expand Down
9 changes: 8 additions & 1 deletion apps/app/src/api/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,15 @@ pub async fn profile_add_project_from_version(
path: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
) -> Result<String> {
Ok(profile::add_project_from_version(path, version_id, reason).await?)
Ok(profile::add_project_from_version(
path,
version_id,
reason,
dependent_on_version_id,
)
.await?)
}

// Adds a project to a profile from a path
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion apps/labrinth/src/clickhouse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ pub async fn init_client_with_database(
ALTER TABLE {database}.{DOWNLOADS} {cluster_line}
ADD COLUMN IF NOT EXISTS reason String,
ADD COLUMN IF NOT EXISTS game_version String,
ADD COLUMN IF NOT EXISTS loader String
ADD COLUMN IF NOT EXISTS loader String,
ADD COLUMN IF NOT EXISTS dependent_on_version_id UInt64
"
))
.execute()
Expand Down
1 change: 1 addition & 0 deletions apps/labrinth/src/models/v3/analytics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub struct Download {
pub reason: String,
pub game_version: String,
pub loader: String,
pub dependent_on_version_id: u64,
}

/// Why a project was downloaded.
Expand Down
98 changes: 92 additions & 6 deletions apps/labrinth/src/routes/internal/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::auth::validate::get_user_record_from_bearer_token;
use crate::database::PgPool;
use crate::database::redis::RedisPool;
use crate::models::analytics::{Download, DownloadReason};
use crate::models::ids::ProjectId;
use crate::models::ids::{ProjectId, VersionId};
use crate::models::pats::Scopes;
use crate::queue::analytics::AnalyticsQueue;
use crate::queue::session::AuthQueue;
Expand All @@ -13,10 +13,12 @@ use crate::util::error::Context;
use crate::util::guards::admin_key_guard;
use crate::util::tags::valid_download_tags;
use actix_web::{HttpRequest, HttpResponse, patch, post, web};
use ariadne::ids::base62_impl::parse_base62;
use eyre::eyre;
use serde::Deserialize;
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::str::FromStr;
use std::sync::Arc;
use tracing::trace;

Expand Down Expand Up @@ -45,10 +47,86 @@ pub struct DownloadMeta {
pub reason: Option<DownloadReason>,
pub game_version: Option<String>,
pub loader: Option<String>,
pub dependent_on: Option<VersionId>,
}

pub const DOWNLOAD_META_HEADER: &str = "modrinth-download-meta";

fn parse_download_meta_version(
version_id: &str,
field: &str,
) -> Result<VersionId, ApiError> {
parse_base62(version_id)
.map(VersionId)
.wrap_request_err_with(|| {
eyre!("invalid `{field}` version id '{version_id}'")
})
}

fn parse_download_meta_from_query(
url: &url::Url,
) -> Result<Option<DownloadMeta>, ApiError> {
let mut meta = DownloadMeta {
reason: None,
game_version: None,
loader: None,
dependent_on: None,
};

for (key, value) in url.query_pairs() {
match key.as_ref() {
"mr_download_reason" => {
meta.reason =
Some(DownloadReason::from_str(&value).map_err(|_| {
ApiError::Request(eyre!(
"invalid download reason specified"
))
})?);
}
"mr_game_version" => {
meta.game_version = Some(value.into_owned());
}
"mr_loader" => {
meta.loader = Some(value.into_owned());
}
"mr_dependent_on" => {
meta.dependent_on =
Some(parse_download_meta_version(&value, "dependent_on")?);
}
_ => {}
}
}

Ok((meta.reason.is_some()
|| meta.game_version.is_some()
|| meta.loader.is_some()
|| meta.dependent_on.is_some())
.then_some(meta))
}

async fn resolve_download_attribution_version(
pool: &PgPool,
redis: &RedisPool,
version_id: Option<VersionId>,
field: &str,
) -> Result<u64, ApiError> {
let Some(version_id) = version_id else {
return Ok(0);
};

let version_id =
crate::database::models::ids::DBVersionId::from(version_id);

crate::database::models::DBVersion::get(version_id, pool, redis)
.await
.wrap_internal_err("failed to fetch download attribution version")?
.ok_or_else(|| {
ApiError::Request(eyre!("invalid `{field}` version specified"))
})?;

Ok(version_id.0 as u64)
}

// This is an internal route, cannot be used without key
#[utoipa::path(
patch,
Expand Down Expand Up @@ -89,10 +167,9 @@ pub async fn count_download(
let project_id: crate::database::models::ids::DBProjectId =
download_body.project_id.into();

let id_option =
ariadne::ids::base62_impl::parse_base62(&download_body.version_name)
.ok()
.map(|x| x as i64);
let id_option = parse_base62(&download_body.version_name)
.ok()
.map(|x| x as i64);

let (version_id, project_id) = if let Some(version) = sqlx::query!(
"
Expand Down Expand Up @@ -138,7 +215,7 @@ pub async fn count_download(
.map(Some)
.wrap_request_err("invalid download meta")?
} else {
None
parse_download_meta_from_query(&url)?
};

if let Some(meta) = &meta {
Expand All @@ -162,6 +239,14 @@ pub async fn count_download(
}
}

let dependent_on_version_id = resolve_download_attribution_version(
&pool,
&redis,
meta.as_ref().and_then(|m| m.dependent_on),
"dependent_on",
)
.await?;

let download = Download {
recorded: get_current_tenths_of_ms(),
domain: url.host_str().unwrap_or_default().to_string(),
Expand Down Expand Up @@ -212,6 +297,7 @@ pub async fn count_download(
.and_then(|m| m.loader.as_ref())
.map(|s| s.to_string())
.unwrap_or_default(),
dependent_on_version_id,
};
trace!("added download {download:#?}");

Expand Down
Loading
Loading