Have app send download analytics meta (#5954)
* wip: add download reasons to app * update how download meta is gathered * cargo fmt * prepr frontend
This commit is contained in:
@@ -107,7 +107,7 @@ defineExpose({
|
|||||||
|
|
||||||
const install = async () => {
|
const install = async () => {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
await installMod(instance.value.path, selectedVersion.value.id, 'standalone').catch(handleError)
|
||||||
installing.value = false
|
installing.value = false
|
||||||
onInstall.value(selectedVersion.value.id)
|
onInstall.value(selectedVersion.value.id)
|
||||||
incompatibleModal.value.hide()
|
incompatibleModal.value.hide()
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ async function install(instance) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await installMod(instance.path, version.id).catch(handleError)
|
await installMod(instance.path, version.id, 'standalone').catch(handleError)
|
||||||
await installVersionDependencies(instance, version).catch(handleError)
|
await installVersionDependencies(instance, version).catch(handleError)
|
||||||
|
|
||||||
instance.installedMod = true
|
instance.installedMod = true
|
||||||
@@ -188,7 +188,7 @@ const createInstance = async () => {
|
|||||||
|
|
||||||
const id = await create(name.value, gameVersion, loader, 'latest', icon.value).catch(handleError)
|
const id = await create(name.value, gameVersion, loader, 'latest', icon.value).catch(handleError)
|
||||||
|
|
||||||
await installMod(id, versions.value[0].id).catch(handleError)
|
await installMod(id, versions.value[0].id, 'standalone').catch(handleError)
|
||||||
|
|
||||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||||
|
|
||||||
|
|||||||
@@ -184,8 +184,18 @@ export async function update_project(path: string, projectPath: string): Promise
|
|||||||
|
|
||||||
// Add a project to a profile from a version
|
// Add a project to a profile from a version
|
||||||
// Returns a path to the new project file
|
// Returns a path to the new project file
|
||||||
export async function add_project_from_version(path: string, versionId: string): Promise<string> {
|
export type DownloadReason = 'standalone' | 'dependency' | 'modpack'
|
||||||
return await invoke('plugin:profile|profile_add_project_from_version', { path, versionId })
|
|
||||||
|
export async function add_project_from_version(
|
||||||
|
path: string,
|
||||||
|
versionId: string,
|
||||||
|
reason: DownloadReason,
|
||||||
|
): Promise<string> {
|
||||||
|
return await invoke('plugin:profile|profile_add_project_from_version', {
|
||||||
|
path,
|
||||||
|
versionId,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a project to a profile from a path + project_type
|
// Add a project to a profile from a path + project_type
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await remove_project(props.instance.path, mod.file_path!)
|
await remove_project(props.instance.path, mod.file_path!)
|
||||||
const newPath = await add_project_from_version(props.instance.path, version.id)
|
const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone')
|
||||||
|
|
||||||
const profile = await get(props.instance.path).catch(handleError)
|
const profile = await get(props.instance.path).catch(handleError)
|
||||||
if (profile) {
|
if (profile) {
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ export function createContentInstall(opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await add_project_from_version(instance.id, version.id)
|
await add_project_from_version(instance.id, version.id, 'standalone')
|
||||||
await installVersionDependencies(
|
await installVersionDependencies(
|
||||||
profile,
|
profile,
|
||||||
version,
|
version,
|
||||||
@@ -484,7 +484,7 @@ export function createContentInstall(opts: {
|
|||||||
)
|
)
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
await add_project_from_version(id, version.id)
|
await add_project_from_version(id, version.id, 'standalone')
|
||||||
await opts.router.push(`/instance/${encodeURIComponent(id)}/`)
|
await opts.router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||||
|
|
||||||
const instance = await get(id)
|
const instance = await get(id)
|
||||||
@@ -585,7 +585,7 @@ export function createContentInstall(opts: {
|
|||||||
const installedProjectIds: string[] = [project.id]
|
const installedProjectIds: string[] = [project.id]
|
||||||
addInstallingItem(instancePath, project, version)
|
addInstallingItem(instancePath, project, version)
|
||||||
try {
|
try {
|
||||||
await add_project_from_version(instance.path, version.id)
|
await add_project_from_version(instance.path, version.id, 'standalone')
|
||||||
await installVersionDependencies(
|
await installVersionDependencies(
|
||||||
instance,
|
instance,
|
||||||
version,
|
version,
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export const installVersionDependencies = async (profile, version, onDepInstalli
|
|||||||
const batch = queuedInstalls.slice(i, i + batchSize)
|
const batch = queuedInstalls.slice(i, i + batchSize)
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
batch.map(async ({ versionId }) => {
|
batch.map(async ({ versionId }) => {
|
||||||
await add_project_from_version(profile.path, versionId)
|
await add_project_from_version(profile.path, versionId, 'dependency')
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use path_util::SafeRelativeUtf8UnixPathBuf;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use theseus::DownloadReason;
|
||||||
use theseus::data::{ContentItem, Dependency, LinkedModpackInfo};
|
use theseus::data::{ContentItem, Dependency, LinkedModpackInfo};
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
use theseus::profile::QuickPlayType;
|
use theseus::profile::QuickPlayType;
|
||||||
@@ -249,8 +250,9 @@ pub async fn profile_update_project(
|
|||||||
pub async fn profile_add_project_from_version(
|
pub async fn profile_add_project_from_version(
|
||||||
path: &str,
|
path: &str,
|
||||||
version_id: &str,
|
version_id: &str,
|
||||||
|
reason: DownloadReason,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
Ok(profile::add_project_from_version(path, version_id).await?)
|
Ok(profile::add_project_from_version(path, version_id, reason).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a project to a profile from a path
|
// Adds a project to a profile from a path
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
Some((&loading_bar, 80.0)),
|
Some((&loading_bar, 80.0)),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
|
|||||||
@@ -78,9 +78,14 @@ pub async fn import_curseforge(
|
|||||||
thumbnail_url: Some(thumbnail_url),
|
thumbnail_url: Some(thumbnail_url),
|
||||||
}) = minecraft_instance.installed_modpack.clone()
|
}) = minecraft_instance.installed_modpack.clone()
|
||||||
{
|
{
|
||||||
let icon_bytes =
|
let icon_bytes = fetch(
|
||||||
fetch(&thumbnail_url, None, &state.fetch_semaphore, &state.pool)
|
&thumbnail_url,
|
||||||
.await?;
|
None,
|
||||||
|
None,
|
||||||
|
&state.fetch_semaphore,
|
||||||
|
&state.pool,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let filename = thumbnail_url.rsplit('/').next_back();
|
let filename = thumbnail_url.rsplit('/').next_back();
|
||||||
if let Some(filename) = filename {
|
if let Some(filename) = filename {
|
||||||
icon = Some(
|
icon = Some(
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ use crate::data::ModLoader;
|
|||||||
use crate::event::emit::{emit_loading, init_loading};
|
use crate::event::emit::{emit_loading, init_loading};
|
||||||
use crate::event::{LoadingBarId, LoadingBarType};
|
use crate::event::{LoadingBarId, LoadingBarType};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
CacheBehaviour, CachedEntry, LinkedData, ProfileInstallStage, SideType,
|
CacheBehaviour, CachedEntry, LinkedData, Profile, ProfileInstallStage,
|
||||||
|
SideType,
|
||||||
|
};
|
||||||
|
use crate::util::fetch::{
|
||||||
|
DownloadMeta, DownloadReason, fetch, fetch_advanced, write_cached_icon,
|
||||||
};
|
};
|
||||||
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
|
|
||||||
use crate::util::io;
|
use crate::util::io;
|
||||||
|
|
||||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||||
@@ -287,12 +290,29 @@ pub async fn generate_pack_from_version_id(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let profile =
|
||||||
|
Profile::get(&profile_path, &state.pool)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::UnmanagedProfileError(
|
||||||
|
profile_path.to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let download_meta = DownloadMeta {
|
||||||
|
reason: DownloadReason::Modpack,
|
||||||
|
game_version: profile.game_version.clone(),
|
||||||
|
loader: profile.loader.as_str().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let file = fetch_advanced(
|
let file = fetch_advanced(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&url,
|
&url,
|
||||||
hash.map(|x| &**x),
|
hash.map(|x| &**x),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
Some(&download_meta),
|
||||||
Some((&loading_bar, 70.0)),
|
Some((&loading_bar, 70.0)),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
@@ -320,9 +340,14 @@ pub async fn generate_pack_from_version_id(
|
|||||||
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?;
|
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?;
|
||||||
let fetched = if let Some(icon_url) = project.icon_url {
|
let fetched = if let Some(icon_url) = project.icon_url {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let icon_bytes =
|
let icon_bytes = fetch(
|
||||||
fetch(&icon_url, None, &state.fetch_semaphore, &state.pool)
|
&icon_url,
|
||||||
.await?;
|
None,
|
||||||
|
None,
|
||||||
|
&state.fetch_semaphore,
|
||||||
|
&state.pool,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let filename = icon_url.rsplit('/').next();
|
let filename = icon_url.rsplit('/').next();
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ use crate::pack::install_from::{
|
|||||||
EnvType, PackFile, PackFileHash, set_profile_information,
|
EnvType, PackFile, PackFileHash, set_profile_information,
|
||||||
};
|
};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
CacheBehaviour, CachedEntry, ProfileInstallStage, SideType, cache_file_hash,
|
CacheBehaviour, CachedEntry, Profile, ProfileInstallStage, SideType,
|
||||||
|
cache_file_hash,
|
||||||
|
};
|
||||||
|
use crate::util::fetch::{
|
||||||
|
DownloadMeta, DownloadReason, fetch_mirrors, sha1_async, write,
|
||||||
};
|
};
|
||||||
use crate::util::fetch::{fetch_mirrors, sha1_async, write};
|
|
||||||
use crate::util::io;
|
use crate::util::io;
|
||||||
use crate::{State, profile};
|
use crate::{State, profile};
|
||||||
use async_zip::base::read::seek::ZipFileReader;
|
use async_zip::base::read::seek::ZipFileReader;
|
||||||
@@ -207,6 +210,22 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let profile =
|
||||||
|
Profile::get(&profile_path, &state.pool)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::UnmanagedProfileError(
|
||||||
|
profile_path.to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let download_meta = DownloadMeta {
|
||||||
|
reason: DownloadReason::Modpack,
|
||||||
|
game_version: profile.game_version.clone(),
|
||||||
|
loader: profile.loader.as_str().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let num_files = pack.files.len();
|
let num_files = pack.files.len();
|
||||||
loading_try_for_each_concurrent(
|
loading_try_for_each_concurrent(
|
||||||
futures::stream::iter(pack.files.into_iter())
|
futures::stream::iter(pack.files.into_iter())
|
||||||
@@ -218,6 +237,7 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
None,
|
None,
|
||||||
|project| {
|
|project| {
|
||||||
let profile_path = profile_path.clone();
|
let profile_path = profile_path.clone();
|
||||||
|
let download_meta = download_meta.clone();
|
||||||
async move {
|
async move {
|
||||||
//TODO: Future update: prompt user for optional files in a modpack
|
//TODO: Future update: prompt user for optional files in a modpack
|
||||||
if let Some(env) = project.env
|
if let Some(env) = project.env
|
||||||
@@ -235,6 +255,7 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
.map(|x| &**x)
|
.map(|x| &**x)
|
||||||
.collect::<Vec<&str>>(),
|
.collect::<Vec<&str>>(),
|
||||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||||
|
Some(&download_meta),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ pub async fn profile_create(
|
|||||||
let fetched = crate::util::fetch::fetch(
|
let fetched = crate::util::fetch::fetch(
|
||||||
icon,
|
icon,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -461,6 +461,7 @@ pub async fn update_project(
|
|||||||
let mut path = Profile::add_project_version(
|
let mut path = Profile::add_project_version(
|
||||||
profile_path,
|
profile_path,
|
||||||
update_version,
|
update_version,
|
||||||
|
fetch::DownloadReason::Standalone,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&state.io_semaphore,
|
&state.io_semaphore,
|
||||||
@@ -501,11 +502,14 @@ pub async fn update_project(
|
|||||||
pub async fn add_project_from_version(
|
pub async fn add_project_from_version(
|
||||||
profile_path: &str,
|
profile_path: &str,
|
||||||
version_id: &str,
|
version_id: &str,
|
||||||
|
reason: fetch::DownloadReason,
|
||||||
) -> crate::Result<String> {
|
) -> crate::Result<String> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|
||||||
let project_path = Profile::add_project_version(
|
let project_path = Profile::add_project_version(
|
||||||
profile_path,
|
profile_path,
|
||||||
version_id,
|
version_id,
|
||||||
|
reason,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
&state.io_semaphore,
|
&state.io_semaphore,
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ pub async fn download_client(
|
|||||||
let bytes = fetch(
|
let bytes = fetch(
|
||||||
&client_download.url,
|
&client_download.url,
|
||||||
Some(&client_download.sha1),
|
Some(&client_download.sha1),
|
||||||
|
None,
|
||||||
&st.fetch_semaphore,
|
&st.fetch_semaphore,
|
||||||
&st.pool,
|
&st.pool,
|
||||||
)
|
)
|
||||||
@@ -238,7 +239,7 @@ pub async fn download_assets(
|
|||||||
async {
|
async {
|
||||||
if !resource_path.exists() || force {
|
if !resource_path.exists() || force {
|
||||||
let resource = fetch_cell
|
let resource = fetch_cell
|
||||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &st.pool))
|
.get_or_try_init(|| fetch(&url, Some(hash), None, &st.fetch_semaphore, &st.pool))
|
||||||
.await?;
|
.await?;
|
||||||
write(&resource_path, resource, &st.io_semaphore).await?;
|
write(&resource_path, resource, &st.io_semaphore).await?;
|
||||||
tracing::trace!("Fetched asset with hash {hash}");
|
tracing::trace!("Fetched asset with hash {hash}");
|
||||||
@@ -252,7 +253,7 @@ pub async fn download_assets(
|
|||||||
|
|
||||||
if with_legacy && !resource_path.exists() || force {
|
if with_legacy && !resource_path.exists() || force {
|
||||||
let resource = fetch_cell
|
let resource = fetch_cell
|
||||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &st.pool))
|
.get_or_try_init(|| fetch(&url, Some(hash), None, &st.fetch_semaphore, &st.pool))
|
||||||
.await?;
|
.await?;
|
||||||
write(&resource_path, resource, &st.io_semaphore).await?;
|
write(&resource_path, resource, &st.io_semaphore).await?;
|
||||||
tracing::trace!("Fetched legacy asset with hash {hash}");
|
tracing::trace!("Fetched legacy asset with hash {hash}");
|
||||||
@@ -326,6 +327,7 @@ pub async fn download_libraries(
|
|||||||
let data = fetch(
|
let data = fetch(
|
||||||
&native.url,
|
&native.url,
|
||||||
Some(&native.sha1),
|
Some(&native.sha1),
|
||||||
|
None,
|
||||||
&st.fetch_semaphore,
|
&st.fetch_semaphore,
|
||||||
&st.pool,
|
&st.pool,
|
||||||
)
|
)
|
||||||
@@ -370,6 +372,7 @@ pub async fn download_libraries(
|
|||||||
let bytes = fetch(
|
let bytes = fetch(
|
||||||
&artifact.url,
|
&artifact.url,
|
||||||
Some(&artifact.sha1),
|
Some(&artifact.sha1),
|
||||||
|
None,
|
||||||
&st.fetch_semaphore,
|
&st.fetch_semaphore,
|
||||||
&st.pool,
|
&st.pool,
|
||||||
)
|
)
|
||||||
@@ -406,7 +409,8 @@ pub async fn download_libraries(
|
|||||||
// failed download here is not a fatal condition.
|
// failed download here is not a fatal condition.
|
||||||
//
|
//
|
||||||
// See DEV-479.
|
// See DEV-479.
|
||||||
match fetch(&url, None, &st.fetch_semaphore, &st.pool).await
|
match fetch(&url, None, None, &st.fetch_semaphore, &st.pool)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
write(&path, &bytes, &st.io_semaphore).await?;
|
write(&path, &bytes, &st.io_semaphore).await?;
|
||||||
@@ -465,6 +469,7 @@ pub async fn download_log_config(
|
|||||||
let bytes = fetch(
|
let bytes = fetch(
|
||||||
&log_download.url,
|
&log_download.url,
|
||||||
Some(&log_download.sha1),
|
Some(&log_download.sha1),
|
||||||
|
None,
|
||||||
&st.fetch_semaphore,
|
&st.fetch_semaphore,
|
||||||
&st.pool,
|
&st.pool,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub use event::{
|
|||||||
};
|
};
|
||||||
pub use logger::start_logger;
|
pub use logger::start_logger;
|
||||||
pub use state::State;
|
pub use state::State;
|
||||||
|
pub use util::fetch::DownloadReason;
|
||||||
|
|
||||||
pub fn launcher_user_agent() -> String {
|
pub fn launcher_user_agent() -> String {
|
||||||
const LAUNCHER_BASE_USER_AGENT: &str =
|
const LAUNCHER_BASE_USER_AGENT: &str =
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ impl FriendsSocket {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
exec,
|
exec,
|
||||||
)
|
)
|
||||||
@@ -358,6 +359,7 @@ impl FriendsSocket {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
exec,
|
exec,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
use crate::pack::install_from::{PackFileHash, PackFormat};
|
use crate::pack::install_from::{PackFileHash, PackFormat};
|
||||||
use crate::state::profiles::{Profile, ProfileFile, ProjectType};
|
use crate::state::profiles::{Profile, ProfileFile, ProjectType};
|
||||||
use crate::state::{CacheBehaviour, CachedEntry};
|
use crate::state::{CacheBehaviour, CachedEntry};
|
||||||
use crate::util::fetch::{FetchSemaphore, fetch_mirrors, sha1_async};
|
use crate::util::fetch::{
|
||||||
|
DownloadMeta, DownloadReason, FetchSemaphore, fetch_mirrors, sha1_async,
|
||||||
|
};
|
||||||
use async_zip::base::read::seek::ZipFileReader;
|
use async_zip::base::read::seek::ZipFileReader;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
@@ -319,6 +321,7 @@ pub async fn get_content_items(
|
|||||||
);
|
);
|
||||||
match get_modpack_identifiers(
|
match get_modpack_identifiers(
|
||||||
&linked_data.version_id,
|
&linked_data.version_id,
|
||||||
|
profile,
|
||||||
pool,
|
pool,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
)
|
)
|
||||||
@@ -638,6 +641,7 @@ pub async fn get_linked_modpack_content(
|
|||||||
|
|
||||||
let modpack_ids = match get_modpack_identifiers(
|
let modpack_ids = match get_modpack_identifiers(
|
||||||
&linked_data.version_id,
|
&linked_data.version_id,
|
||||||
|
profile,
|
||||||
pool,
|
pool,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
)
|
)
|
||||||
@@ -802,6 +806,7 @@ impl ModpackIdentifiers {
|
|||||||
/// Checks cache first, falls back to downloading mrpack if not cached.
|
/// Checks cache first, falls back to downloading mrpack if not cached.
|
||||||
async fn get_modpack_identifiers(
|
async fn get_modpack_identifiers(
|
||||||
version_id: &str,
|
version_id: &str,
|
||||||
|
profile: &crate::state::Profile,
|
||||||
pool: &SqlitePool,
|
pool: &SqlitePool,
|
||||||
fetch_semaphore: &FetchSemaphore,
|
fetch_semaphore: &FetchSemaphore,
|
||||||
) -> crate::Result<ModpackIdentifiers> {
|
) -> crate::Result<ModpackIdentifiers> {
|
||||||
@@ -880,9 +885,16 @@ async fn get_modpack_identifiers(
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let download_meta = DownloadMeta {
|
||||||
|
reason: DownloadReason::Modpack,
|
||||||
|
game_version: profile.game_version.clone(),
|
||||||
|
loader: profile.loader.as_str().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let mrpack_bytes = fetch_mirrors(
|
let mrpack_bytes = fetch_mirrors(
|
||||||
&[&primary_file.url],
|
&[&primary_file.url],
|
||||||
primary_file.hashes.get("sha1").map(|s| s.as_str()),
|
primary_file.hashes.get("sha1").map(|s| s.as_str()),
|
||||||
|
Some(&download_meta),
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
pool,
|
pool,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ impl ModrinthCredentials {
|
|||||||
None,
|
None,
|
||||||
Some(("Authorization", &*creds.session)),
|
Some(("Authorization", &*creds.session)),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
exec,
|
exec,
|
||||||
)
|
)
|
||||||
@@ -226,6 +227,7 @@ async fn fetch_info(
|
|||||||
None,
|
None,
|
||||||
Some(("Authorization", token)),
|
Some(("Authorization", token)),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
exec,
|
exec,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1110,10 +1110,25 @@ impl Profile {
|
|||||||
pub async fn add_project_version(
|
pub async fn add_project_version(
|
||||||
profile_path: &str,
|
profile_path: &str,
|
||||||
version_id: &str,
|
version_id: &str,
|
||||||
|
reason: util::fetch::DownloadReason,
|
||||||
pool: &SqlitePool,
|
pool: &SqlitePool,
|
||||||
fetch_semaphore: &FetchSemaphore,
|
fetch_semaphore: &FetchSemaphore,
|
||||||
io_semaphore: &IoSemaphore,
|
io_semaphore: &IoSemaphore,
|
||||||
) -> crate::Result<String> {
|
) -> crate::Result<String> {
|
||||||
|
let profile =
|
||||||
|
Self::get(profile_path, pool).await?.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::UnmanagedProfileError(
|
||||||
|
profile_path.to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let download_meta = util::fetch::DownloadMeta {
|
||||||
|
reason,
|
||||||
|
game_version: profile.game_version.clone(),
|
||||||
|
loader: profile.loader.as_str().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let version =
|
let version =
|
||||||
CachedEntry::get_version(version_id, None, pool, fetch_semaphore)
|
CachedEntry::get_version(version_id, None, pool, fetch_semaphore)
|
||||||
.await?
|
.await?
|
||||||
@@ -1139,6 +1154,7 @@ impl Profile {
|
|||||||
let bytes = util::fetch::fetch(
|
let bytes = util::fetch::fetch(
|
||||||
&file.url,
|
&file.url,
|
||||||
file.hashes.get("sha1").map(|x| &**x),
|
file.hashes.get("sha1").map(|x| &**x),
|
||||||
|
Some(&download_meta),
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
pool,
|
pool,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use parking_lot::Mutex;
|
|||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -17,6 +18,30 @@ use std::time::{self};
|
|||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tokio::{fs::File, io::AsyncWriteExt};
|
use tokio::{fs::File, io::AsyncWriteExt};
|
||||||
|
|
||||||
|
pub const DOWNLOAD_META_HEADER: &str = "modrinth-download-meta";
|
||||||
|
|
||||||
|
#[derive(Debug, derive_more::Display, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[display(rename_all = "snake_case")]
|
||||||
|
pub enum DownloadReason {
|
||||||
|
Standalone,
|
||||||
|
Dependency,
|
||||||
|
Modpack,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DownloadMeta {
|
||||||
|
pub reason: DownloadReason,
|
||||||
|
pub game_version: String,
|
||||||
|
pub loader: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DownloadMeta {
|
||||||
|
pub fn to_header_value(&self) -> String {
|
||||||
|
serde_json::to_string(self).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct IoSemaphore(pub Semaphore);
|
pub struct IoSemaphore(pub Semaphore);
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -156,17 +181,29 @@ const FETCH_ATTEMPTS: usize = 2;
|
|||||||
pub async fn fetch(
|
pub async fn fetch(
|
||||||
url: &str,
|
url: &str,
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
|
download_meta: Option<&DownloadMeta>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||||
) -> crate::Result<Bytes> {
|
) -> crate::Result<Bytes> {
|
||||||
fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore, exec)
|
fetch_advanced(
|
||||||
.await
|
Method::GET,
|
||||||
|
url,
|
||||||
|
sha1,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
download_meta,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
exec,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(semaphore))]
|
#[tracing::instrument(skip(semaphore))]
|
||||||
pub async fn fetch_with_client(
|
pub async fn fetch_with_client(
|
||||||
url: &str,
|
url: &str,
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
|
download_meta: Option<&DownloadMeta>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
@@ -177,6 +214,7 @@ pub async fn fetch_with_client(
|
|||||||
sha1,
|
sha1,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
download_meta,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
exec,
|
exec,
|
||||||
@@ -198,7 +236,7 @@ where
|
|||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let result = fetch_advanced(
|
let result = fetch_advanced(
|
||||||
method, url, sha1, json_body, None, None, semaphore, exec,
|
method, url, sha1, json_body, None, None, None, semaphore, exec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let value = serde_json::from_slice(&result)?;
|
let value = serde_json::from_slice(&result)?;
|
||||||
@@ -215,6 +253,7 @@ pub async fn fetch_advanced(
|
|||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
json_body: Option<serde_json::Value>,
|
json_body: Option<serde_json::Value>,
|
||||||
header: Option<(&str, &str)>,
|
header: Option<(&str, &str)>,
|
||||||
|
download_meta: Option<&DownloadMeta>,
|
||||||
loading_bar: Option<(&LoadingBarId, f64)>,
|
loading_bar: Option<(&LoadingBarId, f64)>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||||
@@ -225,6 +264,7 @@ pub async fn fetch_advanced(
|
|||||||
sha1,
|
sha1,
|
||||||
json_body,
|
json_body,
|
||||||
header,
|
header,
|
||||||
|
download_meta,
|
||||||
loading_bar,
|
loading_bar,
|
||||||
semaphore,
|
semaphore,
|
||||||
exec,
|
exec,
|
||||||
@@ -242,6 +282,7 @@ pub async fn fetch_advanced_with_client(
|
|||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
json_body: Option<serde_json::Value>,
|
json_body: Option<serde_json::Value>,
|
||||||
header: Option<(&str, &str)>,
|
header: Option<(&str, &str)>,
|
||||||
|
download_meta: Option<&DownloadMeta>,
|
||||||
loading_bar: Option<(&LoadingBarId, f64)>,
|
loading_bar: Option<(&LoadingBarId, f64)>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||||
@@ -262,12 +303,15 @@ pub async fn fetch_advanced_with_client(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let download_meta_header = download_meta
|
||||||
|
.map(|m| (DOWNLOAD_META_HEADER.to_string(), m.to_header_value()));
|
||||||
|
|
||||||
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
|
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
|
||||||
if is_api_url && GLOBAL_FETCH_FENCE.is_blocked() {
|
if is_api_url && GLOBAL_FETCH_FENCE.is_blocked() {
|
||||||
return Err(ErrorKind::ApiIsDownError.into());
|
return Err(ErrorKind::ApiIsDownError.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut req = INSECURE_REQWEST_CLIENT.request(method.clone(), url);
|
let mut req = client.request(method.clone(), url);
|
||||||
|
|
||||||
if let Some(body) = json_body.clone() {
|
if let Some(body) = json_body.clone() {
|
||||||
req = req.json(&body);
|
req = req.json(&body);
|
||||||
@@ -281,6 +325,11 @@ pub async fn fetch_advanced_with_client(
|
|||||||
req = req.header("Authorization", &creds.session);
|
req = req.header("Authorization", &creds.session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((name, value)) = &download_meta_header {
|
||||||
|
tracing::info!("Sending download analytics: {value}");
|
||||||
|
req = req.header(name.as_str(), value.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
let result = req.send().await;
|
let result = req.send().await;
|
||||||
match result {
|
match result {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
@@ -375,6 +424,7 @@ pub async fn fetch_advanced_with_client(
|
|||||||
pub async fn fetch_mirrors(
|
pub async fn fetch_mirrors(
|
||||||
mirrors: &[&str],
|
mirrors: &[&str],
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
|
download_meta: Option<&DownloadMeta>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||||
) -> crate::Result<Bytes> {
|
) -> crate::Result<Bytes> {
|
||||||
@@ -385,9 +435,15 @@ pub async fn fetch_mirrors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (index, mirror) in mirrors.iter().enumerate() {
|
for (index, mirror) in mirrors.iter().enumerate() {
|
||||||
let result =
|
let result = fetch_with_client(
|
||||||
fetch_with_client(mirror, sha1, semaphore, exec, &REQWEST_CLIENT)
|
mirror,
|
||||||
.await;
|
sha1,
|
||||||
|
download_meta,
|
||||||
|
semaphore,
|
||||||
|
exec,
|
||||||
|
&REQWEST_CLIENT,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user