my own take on some BASED design decisions

This commit is contained in:
DecDuck
2024-10-24 22:05:58 +11:00
parent 10c8344930
commit 5ed0833e61
11 changed files with 130 additions and 143 deletions

View File

@ -1,18 +1,31 @@
<template> <template>
<button
<button class="w-full rounded-md p-4 bg-blue-600 text-white" @click="requestGameWrapper"> class="w-full rounded-md p-4 bg-blue-600 text-white"
@click="requestGameWrapper"
>
Load Data Load Data
</button> </button>
<button class="w-full rounded-md p-4 bg-blue-600 text-white" @click="requestGameWrapper"> <input placeholder="GAME ID" v-model="gameId" />
<input placehodler="VERSION NAME" v-model="versionName" />
<button
class="w-full rounded-md p-4 bg-blue-600 text-white"
@click="requestGameWrapper"
>
Download Game Download Game
</button> </button>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
const gameId = ref("");
const versionName = ref("");
async function requestGame() { async function requestGame() {
console.log("Requested game from FE"); await invoke("start_game_download", {
await invoke("start_game_download", { gameId: "328a276d-4777-4a47-97f1-15069c1e5f66", gameVersion: "1.11.2", maxThreads: 4 }); gameId: gameId.value,
gameVersion: versionName.value,
maxThreads: 4,
});
} }
function requestGameWrapper() { function requestGameWrapper() {
console.log("Wrapper started"); console.log("Wrapper started");
@ -20,6 +33,6 @@ function requestGameWrapper() {
.then(() => {}) .then(() => {})
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}) });
} }
</script> </script>

7
src-tauri/Cargo.lock generated
View File

@ -1039,6 +1039,7 @@ dependencies = [
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tokio", "tokio",
"url", "url",
"urlencoding",
"uuid", "uuid",
"versions", "versions",
"webbrowser", "webbrowser",
@ -4815,6 +4816,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "urlpattern" name = "urlpattern"
version = "0.3.0" version = "0.3.0"

View File

@ -39,6 +39,7 @@ env_logger = "0.11.5"
http = "1.1.0" http = "1.1.0"
tokio = { version = "1.40.0", features = ["rt", "tokio-macros"] } tokio = { version = "1.40.0", features = ["rt", "tokio-macros"] }
versions = { version = "6.3.2", features = ["serde"] } versions = { version = "6.3.2", features = ["serde"] }
urlencoding = "2.1.3"
[dependencies.uuid] [dependencies.uuid]
version = "1.10.0" version = "1.10.0"

View File

@ -1,47 +0,0 @@
use std::fs::File;
use std::io::{Seek, SeekFrom, Write};
use std::os::unix::fs::MetadataExt;
use std::sync::{Arc, Mutex};
use log::info;
use uuid::Bytes;
use crate::auth::generate_authorization_header;
use crate::DB;
use crate::db::DatabaseImpls;
use crate::downloads::manifest::DropDownloadContext;
const CHUNK_SIZE: u64 = 1024 * 1024 * 64;
pub fn download_game_chunk(ctx: DropDownloadContext) {
let base_url = DB.fetch_base_url();
let client = reqwest::blocking::Client::new();
let chunk_url = base_url.join(
&format!(
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
ctx.game_id,
ctx.version,
ctx.file_name,
ctx.index
)).unwrap();
let header = generate_authorization_header();
let response = client
.get(chunk_url)
.header("Authorization", header)
.send()
.unwrap();
let response_data = response.bytes().unwrap();
write_to_file(ctx.file, ctx.index as u64, response_data.to_vec());
}
fn write_to_file(file: Arc<Mutex<File>>, index: u64, data: Vec<u8>) {
let mut lock = file.lock().unwrap();
if index != 0 {
lock.seek(SeekFrom::Start(index * CHUNK_SIZE)).expect("Failed to seek to file offset");
}
lock.write_all(&data).unwrap();
}

View File

@ -0,0 +1,40 @@
use crate::auth::generate_authorization_header;
use crate::db::DatabaseImpls;
use crate::downloads::manifest::DropDownloadContext;
use crate::DB;
use log::info;
use urlencoding::encode;
use std::io::{BufWriter, Seek, SeekFrom, Write};
pub fn download_game_chunk(ctx: DropDownloadContext) {
let base_url = DB.fetch_base_url();
let client = reqwest::blocking::Client::new();
let chunk_url = base_url
.join(&format!(
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
// Encode the parts we don't trust
ctx.game_id, encode(&ctx.version), encode(&ctx.file_name), ctx.index
))
.unwrap();
let header = generate_authorization_header();
let mut response = client
.get(chunk_url)
.header("Authorization", header)
.send()
.unwrap();
let mut file_lock = ctx.file.lock().unwrap();
if ctx.offset != 0 {
file_lock
.seek(SeekFrom::Start(ctx.offset))
.expect("Failed to seek to file offset");
}
let mut stream = BufWriter::with_capacity(1024, file_lock.try_clone().unwrap());
response.copy_to(&mut stream).unwrap();
}

View File

@ -1,19 +1,19 @@
use std::fs::File;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::atomic::AtomicUsize;
use log::info;
use serde::{Deserialize, Serialize};
use crate::{AppState, DB};
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::db::{DatabaseImpls, DATA_ROOT_DIR}; use crate::db::{DatabaseImpls, DATA_ROOT_DIR};
use crate::downloads::download_files; use crate::downloads::download_logic;
use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::downloads::progress::ProgressChecker; use crate::downloads::progress::ProgressChecker;
use crate::{AppState, DB};
use log::info;
use serde::{Deserialize, Serialize};
use std::fs::{create_dir_all, File};
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, Mutex};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GameDownload { pub struct GameDownloadManager {
id: String, id: String,
version: String, version: String,
progress: Arc<AtomicUsize>, progress: Arc<AtomicUsize>,
@ -40,14 +40,14 @@ pub enum GameDownloadError {
} }
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
pub enum SystemError { pub enum SystemError {
MutexLockFailed MutexLockFailed,
} }
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GameChunkCtx { pub struct GameChunkCtx {
chunk_id: usize, chunk_id: usize,
} }
impl GameDownload { impl GameDownloadManager {
pub fn new(id: String, version: String) -> Self { pub fn new(id: String, version: String) -> Self {
Self { Self {
id, id,
@ -64,11 +64,16 @@ impl GameDownload {
} }
self.ensure_manifest_exists().await self.ensure_manifest_exists().await
} }
pub async fn download(&self, max_threads: usize, contexts: Vec<DropDownloadContext>) -> Result<(), GameDownloadError> { pub fn begin_download(
&self,
max_threads: usize,
contexts: Vec<DropDownloadContext>,
) -> Result<(), GameDownloadError> {
let progress = Arc::new(AtomicUsize::new(0)); let progress = Arc::new(AtomicUsize::new(0));
self.change_state(GameDownloadState::Downloading); self.change_state(GameDownloadState::Downloading);
let progress = ProgressChecker::new(Box::new(download_files::download_game_chunk), progress); let progress =
progress.run_contexts_parallel_async(contexts, max_threads).await; ProgressChecker::new(Box::new(download_logic::download_game_chunk), progress);
progress.run_contexts_parallel(contexts, max_threads);
Ok(()) Ok(())
} }
pub async fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> { pub async fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> {
@ -85,10 +90,9 @@ impl GameDownload {
.join( .join(
format!( format!(
"/api/v1/client/metadata/manifest?id={}&version={}", "/api/v1/client/metadata/manifest?id={}&version={}",
self.id, self.id, self.version
self.version
) )
.as_str() .as_str(),
) )
.unwrap(); .unwrap();
@ -109,10 +113,11 @@ impl GameDownload {
} }
let manifest_download = response.json::<DropManifest>().await.unwrap(); let manifest_download = response.json::<DropManifest>().await.unwrap();
info!("Manifest: {:?}", manifest_download);
if let Ok(mut manifest) = self.manifest.lock() { if let Ok(mut manifest) = self.manifest.lock() {
*manifest = Some(manifest_download) *manifest = Some(manifest_download)
} else { return Err(GameDownloadError::System(SystemError::MutexLockFailed)); } } else {
return Err(GameDownloadError::System(SystemError::MutexLockFailed));
}
Ok(()) Ok(())
} }
@ -122,26 +127,38 @@ impl GameDownload {
*lock = state; *lock = state;
} }
} }
pub fn to_contexts(manifest: &DropManifest, version: String, game_id: String) -> Vec<DropDownloadContext> { pub fn generate_job_contexts(
manifest: &DropManifest,
version: String,
game_id: String,
) -> Vec<DropDownloadContext> {
let mut contexts = Vec::new(); let mut contexts = Vec::new();
let base_path = DATA_ROOT_DIR.clone(); let base_path = DATA_ROOT_DIR.join("games").join(game_id.clone()).clone();
for key in manifest { create_dir_all(base_path.clone()).unwrap();
let path = base_path.join(Path::new(key.0)); for (raw_path, chunk) in manifest {
let file = Arc::new(Mutex::new(File::create(path).unwrap())); let path = base_path.join(Path::new(raw_path));
for i in 0..key.1.ids.len() {
contexts.push(DropDownloadContext {
file_chunk: Arc::new(key.1.clone()),
file_name: key.0.clone(), let container = path.parent().unwrap();
create_dir_all(container).unwrap();
let file = Arc::new(Mutex::new(File::create(path).unwrap()));
let mut running_offset = 0;
for i in 0..chunk.ids.len() {
if i == 1 {
info!("woah a chunk bigger than 1")
}
contexts.push(DropDownloadContext {
file_name: raw_path.to_string(),
version: version.to_string(), version: version.to_string(),
offset: running_offset,
index: i, index: i,
game_id: game_id.to_string(), game_id: game_id.to_string(),
file: file.clone(), file: file.clone(),
}); });
running_offset += chunk.lengths[i] as u64;
} }
} }
info!("Contexts: {:?}", contexts);
contexts contexts
} }
@ -154,40 +171,21 @@ pub async fn start_game_download(
) -> Result<(), GameDownloadError> { ) -> Result<(), GameDownloadError> {
info!("Triggered Game Download"); info!("Triggered Game Download");
let download = Arc::new(GameDownload::new(game_id.clone(), game_version.clone())); let download_manager = Arc::new(GameDownloadManager::new(
game_id.clone(),
game_version.clone(),
));
download.ensure_manifest_exists().await?; download_manager.ensure_manifest_exists().await?;
let local_manifest = { let local_manifest = {
let manifest = download.manifest.lock().unwrap(); let manifest = download_manager.manifest.lock().unwrap();
(*manifest).clone().unwrap() (*manifest).clone().unwrap()
}; };
let contexts = to_contexts(&local_manifest, game_version.clone(), game_id); let contexts = generate_job_contexts(&local_manifest, game_version.clone(), game_id);
let _ = download.download(max_threads, contexts).await; let _ = download_manager.begin_download(max_threads, contexts);
Ok(()) Ok(())
/*
let Some(unlocked) = manifest else { return Err(GameDownloadError::ManifestDoesNotExist) };
let lock = unlocked.lock().unwrap();
let chunks = lock.parse_to_chunks();
/*
let manifest = match d.manifest {
Some(lock) => {
let lock = lock.lock().unwrap();
lock.parse_to_chunks()
},
None => { return Err(GameDownloadError::ManifestDoesNotExist) }
};
*/
app_state.game_downloads.push(download.clone());
download.download(max_threads, chunks).await
*/
} }

View File

@ -1,11 +0,0 @@
/* GENERAL OUTLINE
When downloading any game, the following details must be provided to the server:
- Game ID
- User token
- TBC
The steps to then download a game are as follows:
1. User requests
*/

View File

@ -14,10 +14,10 @@ pub struct DropChunk {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DropDownloadContext { pub struct DropDownloadContext {
pub file_chunk: Arc<DropChunk>,
pub file_name: String, pub file_name: String,
pub version: String, pub version: String,
pub index: usize, pub index: usize,
pub offset: u64,
pub game_id: String, pub game_id: String,
pub file: Arc<Mutex<File>> pub file: Arc<Mutex<File>>
} }

View File

@ -1,5 +1,4 @@
mod downloads;
mod manifest; mod manifest;
pub mod progress; pub mod progress;
pub mod game_download; pub mod download_manager;
mod download_files; mod download_logic;

View File

@ -30,19 +30,6 @@ where T: Send + Sync
self.counter.fetch_add(1, Ordering::Relaxed); self.counter.fetch_add(1, Ordering::Relaxed);
} }
} }
pub async fn run_contexts_parallel_async(&self, contexts: Vec<T>, max_threads: usize) {
let threads = ThreadPoolBuilder::new()
// If max_threads == 0, then the limit will be determined
// by Rayon's internal RAYON_NUM_THREADS
.num_threads(max_threads)
.build()
.unwrap();
for context in contexts {
let f = self.f.clone();
threads.spawn(move || f(context));
}
}
pub fn run_contexts_parallel(&self, contexts: Vec<T>, max_threads: usize) { pub fn run_contexts_parallel(&self, contexts: Vec<T>, max_threads: usize) {
let threads = ThreadPoolBuilder::new() let threads = ThreadPoolBuilder::new()
// If max_threads == 0, then the limit will be determined // If max_threads == 0, then the limit will be determined

View File

@ -22,7 +22,7 @@ use std::{
use std::sync::Arc; use std::sync::Arc;
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use crate::db::DatabaseImpls; use crate::db::DatabaseImpls;
use crate::downloads::game_download::{start_game_download, GameDownload}; use crate::downloads::download_manager::{start_game_download, GameDownloadManager};
#[derive(Clone, Copy, Serialize)] #[derive(Clone, Copy, Serialize)]
pub enum AppStatus { pub enum AppStatus {
@ -47,7 +47,7 @@ pub struct AppState {
status: AppStatus, status: AppStatus,
user: Option<User>, user: Option<User>,
games: HashMap<String, Game>, games: HashMap<String, Game>,
game_downloads: Vec<Arc<GameDownload>> game_downloads: Vec<Arc<GameDownloadManager>>
} }
#[tauri::command] #[tauri::command]