mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-12 15:52:43 +10:00
my own take on some BASED design decisions
This commit is contained in:
@ -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
7
src-tauri/Cargo.lock
generated
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
40
src-tauri/src/downloads/download_logic.rs
Normal file
40
src-tauri/src/downloads/download_logic.rs
Normal 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();
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
@ -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>>
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
@ -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
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user