mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-09 20:12:14 +10:00
v2 download API and fixes (#112)
* fix: potential download fixes * fix: show installed games not on remote * fix: more download_logic error handling * partial: move to async * feat: interactivity improvements * feat: v2 download API * fix: download seek offsets * fix: clippy * fix: apply clippy suggestion * fix: performance improvements starting up download * fix: finished bucket file * fix: ui tweaks and fixes * fix: revert version to 0.3.2 * fix: clippy
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-3 inline-flex gap-x-2">
|
||||
<div
|
||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||
@ -67,6 +67,30 @@
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="h-full grow flex p-8 justify-center text-zinc-100"
|
||||
>
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -80,12 +104,12 @@ import { listen } from "@tauri-apps/api/event";
|
||||
// Style information
|
||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Installed]: "text-green-500",
|
||||
[GameStatusEnum.Downloading]: "text-blue-500",
|
||||
[GameStatusEnum.Downloading]: "text-zinc-400",
|
||||
[GameStatusEnum.Validating]: "text-blue-300",
|
||||
[GameStatusEnum.Running]: "text-green-500",
|
||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||
[GameStatusEnum.Queued]: "text-blue-500",
|
||||
[GameStatusEnum.Updating]: "text-blue-500",
|
||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||
@ -107,6 +131,7 @@ const router = useRouter();
|
||||
|
||||
const searchQuery = ref("");
|
||||
|
||||
const loading = ref(false);
|
||||
const games: {
|
||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||
} = {};
|
||||
@ -115,7 +140,10 @@ const icons: { [key: string]: string } = {};
|
||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||
|
||||
async function calculateGames(clearAll = false) {
|
||||
if (clearAll) rawGames.value = [];
|
||||
if (clearAll) {
|
||||
rawGames.value = [];
|
||||
loading.value = true;
|
||||
}
|
||||
// If we update immediately, the navigation gets re-rendered before we
|
||||
// add all the necessary state, and it freaks tf out
|
||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||
@ -127,10 +155,11 @@ async function calculateGames(clearAll = false) {
|
||||
if (icons[game.id]) continue;
|
||||
icons[game.id] = await useObject(game.mIconObjectId);
|
||||
}
|
||||
loading.value = false;
|
||||
rawGames.value = newGames;
|
||||
}
|
||||
|
||||
await calculateGames();
|
||||
calculateGames(true);
|
||||
|
||||
const navigation = computed(() =>
|
||||
rawGames.value.map((game) => {
|
||||
@ -138,8 +167,7 @@ const navigation = computed(() =>
|
||||
|
||||
const isInstalled = computed(
|
||||
() =>
|
||||
status.value.type == GameStatusEnum.Installed ||
|
||||
status.value.type == GameStatusEnum.SetupRequired
|
||||
status.value.type != GameStatusEnum.Remote
|
||||
);
|
||||
|
||||
const item = {
|
||||
|
||||
@ -43,6 +43,7 @@ export const useGame = async (gameId: string) => {
|
||||
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||
|
||||
listen(`update_game/${gameId}`, (event) => {
|
||||
console.log(event);
|
||||
const payload: {
|
||||
status: SerializedGameStatus;
|
||||
version?: GameVersion;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "view",
|
||||
"private": true,
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt generate",
|
||||
|
||||
@ -243,7 +243,10 @@
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||
<div
|
||||
v-else-if="versionOptions === null || versionOptions?.length == 0"
|
||||
class="mt-1 rounded-md bg-red-600/10 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
@ -256,6 +259,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full flex items-center justify-center p-4">
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-7 h-7 text-transparent animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installDirs">
|
||||
<Listbox as="div" v-model="installDir">
|
||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||
@ -510,13 +534,13 @@ async function installFlow() {
|
||||
installDirs.value = undefined;
|
||||
|
||||
try {
|
||||
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||
versionOptions.value = await invoke("fetch_game_version_options", {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
console.log(versionOptions.value);
|
||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||
} catch (error) {
|
||||
installError.value = (error as string).toString();
|
||||
versionOptions.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1284,7 +1284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "drop-app"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"atomic-instant-full",
|
||||
"bitcode",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
fn main() {
|
||||
println!("cargo::rustc-link-lib=appindicator3");
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@ -266,6 +266,7 @@ pub mod data {
|
||||
pub install_dirs: Vec<PathBuf>,
|
||||
// Guaranteed to exist if the game also exists in the app state map
|
||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||
|
||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||
|
||||
|
||||
@ -128,7 +128,7 @@ impl DownloadManagerBuilder {
|
||||
drop(download_thread_lock);
|
||||
}
|
||||
|
||||
fn stop_and_wait_current_download(&self) {
|
||||
fn stop_and_wait_current_download(&self) -> bool {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
if let Some(current_flag) = &self.active_control_flag {
|
||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||
@ -136,8 +136,10 @@ impl DownloadManagerBuilder {
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||
current_download_thread.join().unwrap();
|
||||
}
|
||||
return current_download_thread.join().is_ok();
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn manage_queue(mut self) -> Result<(), ()> {
|
||||
@ -254,12 +256,16 @@ impl DownloadManagerBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
// If the download gets cancelled
|
||||
// If the download gets canceled
|
||||
// immediately return, on_cancelled gets called for us earlier
|
||||
if !download_result {
|
||||
return;
|
||||
}
|
||||
|
||||
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
|
||||
return;
|
||||
}
|
||||
|
||||
let validate_result = match download_agent.validate(&app_handle) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@ -274,6 +280,10 @@ impl DownloadManagerBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
|
||||
return;
|
||||
}
|
||||
|
||||
if validate_result {
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender
|
||||
@ -315,6 +325,7 @@ impl DownloadManagerBuilder {
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
self.set_status(DownloadManagerStatus::Error);
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use std::{
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::Sender,
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@ -23,7 +23,7 @@ pub struct ProgressObject {
|
||||
//last_update: Arc<RwLock<Instant>>,
|
||||
last_update_time: Arc<AtomicInstant>,
|
||||
bytes_last_update: Arc<AtomicUsize>,
|
||||
rolling: RollingProgressWindow<250>,
|
||||
rolling: RollingProgressWindow<1>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -128,7 +128,7 @@ pub fn calculate_update(progress: &ProgressObject) {
|
||||
.bytes_last_update
|
||||
.swap(current_bytes_downloaded, Ordering::Acquire);
|
||||
|
||||
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
|
||||
let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update);
|
||||
|
||||
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ use super::remote_access_error::RemoteAccessError;
|
||||
// TODO: Rename / separate from downloads
|
||||
#[derive(Debug, SerializeDisplay)]
|
||||
pub enum ApplicationDownloadError {
|
||||
NotInitialized,
|
||||
Communication(RemoteAccessError),
|
||||
DiskFull(u64, u64),
|
||||
Checksum,
|
||||
@ -22,6 +23,7 @@ pub enum ApplicationDownloadError {
|
||||
impl Display for ApplicationDownloadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ApplicationDownloadError::NotInitialized => write!(f, "Download not initalized, did something go wrong?"),
|
||||
ApplicationDownloadError::DiskFull(required, available) => write!(
|
||||
f,
|
||||
"Game requires {}, {} remaining left on disk.",
|
||||
@ -39,7 +41,7 @@ impl Display for ApplicationDownloadError {
|
||||
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
||||
ApplicationDownloadError::DownloadError => write!(
|
||||
f,
|
||||
"download failed. See Download Manager status for specific error"
|
||||
"Download failed. See Download Manager status for specific error"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,109 +1,96 @@
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
DB,
|
||||
database::db::DatabaseImpls,
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC},
|
||||
remote::{
|
||||
auth::generate_authorization_header,
|
||||
requests::{generate_url, make_authenticated_get},
|
||||
utils::DROP_CLIENT_ASYNC,
|
||||
},
|
||||
};
|
||||
|
||||
use super::collection::{Collection, Collections};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send()?;
|
||||
pub async fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||
let response =
|
||||
make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?;
|
||||
|
||||
Ok(response.json()?)
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
pub async fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||
let response = make_authenticated_get(generate_url(
|
||||
&["/api/v1/client/collection/", &collection_id],
|
||||
&[],
|
||||
|r| r.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
)?)
|
||||
.await?;
|
||||
|
||||
Ok(response.json()?)
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
|
||||
pub async fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let url = generate_url(&["/api/v1/client/collection"], &[])?;
|
||||
|
||||
let response = client
|
||||
.post(base_url)
|
||||
.post(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.json(&json!({"name": name}))
|
||||
.send()?;
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(response.json()?)
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_game_to_collection(
|
||||
pub async fn add_game_to_collection(
|
||||
collection_id: String,
|
||||
game_id: String,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let url = Url::parse(&format!(
|
||||
"{}api/v1/client/collection/{}/entry/",
|
||||
DB.fetch_base_url(),
|
||||
collection_id
|
||||
))?;
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
|
||||
let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?;
|
||||
|
||||
client
|
||||
.post(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.json(&json!({"id": game_id}))
|
||||
.send()?;
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let base_url = Url::parse(&format!(
|
||||
"{}api/v1/client/collection/{}",
|
||||
DB.fetch_base_url(),
|
||||
collection_id
|
||||
))?;
|
||||
pub async fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
|
||||
let url = generate_url(&["/api/v1/client/collection", &collection_id], &[])?;
|
||||
|
||||
let response = client
|
||||
.delete(base_url)
|
||||
.delete(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()?;
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(response.json()?)
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn delete_game_in_collection(
|
||||
pub async fn delete_game_in_collection(
|
||||
collection_id: String,
|
||||
game_id: String,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let base_url = Url::parse(&format!(
|
||||
"{}api/v1/client/collection/{}/entry",
|
||||
DB.fetch_base_url(),
|
||||
collection_id
|
||||
))?;
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
|
||||
let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?;
|
||||
|
||||
client
|
||||
.delete(base_url)
|
||||
.delete(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.json(&json!({"id": game_id}))
|
||||
.send()?;
|
||||
.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -18,28 +18,28 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
library::{
|
||||
FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic,
|
||||
FetchGameStruct, Game, fetch_game_logic, fetch_game_version_options_logic,
|
||||
fetch_library_logic,
|
||||
},
|
||||
state::{GameStatusManager, GameStatusWithTransient},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_library(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
pub async fn fetch_library(
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
offline!(
|
||||
state,
|
||||
fetch_library_logic,
|
||||
fetch_library_logic_offline,
|
||||
state
|
||||
)
|
||||
).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game(
|
||||
pub async fn fetch_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
offline!(
|
||||
state,
|
||||
@ -47,7 +47,7 @@ pub fn fetch_game(
|
||||
fetch_game_logic_offline,
|
||||
game_id,
|
||||
state
|
||||
)
|
||||
).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -68,9 +68,9 @@ pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), Libr
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fetch_game_verion_options(
|
||||
pub async fn fetch_game_version_options(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
fetch_game_verion_options_logic(game_id, state)
|
||||
fetch_game_version_options_logic(game_id, state).await
|
||||
}
|
||||
|
||||
@ -3,44 +3,47 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
|
||||
use crate::{
|
||||
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
|
||||
download_manager::
|
||||
downloadable::Downloadable
|
||||
,
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
AppState,
|
||||
database::{
|
||||
db::borrow_db_checked,
|
||||
models::data::GameDownloadStatus,
|
||||
},
|
||||
download_manager::downloadable::Downloadable,
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_game(
|
||||
pub async fn download_game(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
install_dir: usize,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), ApplicationDownloadError> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = GameDownloadAgent::new_from_index(
|
||||
game_id,
|
||||
game_version,
|
||||
install_dir,
|
||||
sender,
|
||||
)?;
|
||||
let game_download_agent = Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
|
||||
let sender = { state.lock().unwrap().download_manager.get_sender().clone() };
|
||||
|
||||
let game_download_agent =
|
||||
GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?;
|
||||
|
||||
let game_download_agent =
|
||||
Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent).unwrap();
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent.clone())
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_download(
|
||||
pub async fn resume_download(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), ApplicationDownloadError> {
|
||||
let s = borrow_db_checked()
|
||||
.applications
|
||||
@ -62,17 +65,21 @@ pub fn resume_download(
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let parent_dir: PathBuf = install_dir.into();
|
||||
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||
game_id,
|
||||
version_name.clone(),
|
||||
parent_dir.parent().unwrap().to_path_buf(),
|
||||
sender,
|
||||
)?) as Box<dyn Downloadable + Send + Sync>);
|
||||
let game_download_agent = Arc::new(Box::new(
|
||||
GameDownloadAgent::new(
|
||||
game_id,
|
||||
version_name.clone(),
|
||||
parent_dir.parent().unwrap().to_path_buf(),
|
||||
sender,
|
||||
)
|
||||
.await?,
|
||||
) as Box<dyn Downloadable + Send + Sync>);
|
||||
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent).unwrap();
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -11,13 +11,15 @@ use crate::download_manager::util::download_thread_control_flag::{
|
||||
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::games::downloads::manifest::{
|
||||
DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody,
|
||||
};
|
||||
use crate::games::downloads::validate::validate_game_chunk;
|
||||
use crate::games::library::{on_game_complete, push_game_update, set_partially_installed};
|
||||
use crate::games::state::GameStatusManager;
|
||||
use crate::process::utils::get_disk_available;
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||
use crate::remote::requests::generate_url;
|
||||
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use std::collections::HashMap;
|
||||
@ -31,16 +33,18 @@ use tauri::{AppHandle, Emitter};
|
||||
#[cfg(target_os = "linux")]
|
||||
use rustix::fs::{FallocateFlags, fallocate};
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
use super::download_logic::download_game_bucket;
|
||||
use super::drop_data::DropData;
|
||||
|
||||
static RETRY_COUNT: usize = 3;
|
||||
|
||||
const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000;
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||
buckets: Mutex<Vec<DownloadBucket>>,
|
||||
context_map: Mutex<HashMap<String, bool>>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
@ -50,19 +54,21 @@ pub struct GameDownloadAgent {
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub fn new_from_index(
|
||||
pub async fn new_from_index(
|
||||
id: String,
|
||||
version: String,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
) -> Result<Self, ApplicationDownloadError> {
|
||||
let db_lock = borrow_db_checked();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
let base_dir = {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
Self::new(id, version, base_dir, sender)
|
||||
db_lock.applications.install_dirs[target_download_dir].clone()
|
||||
};
|
||||
|
||||
Self::new(id, version, base_dir, sender).await
|
||||
}
|
||||
pub fn new(
|
||||
pub async fn new(
|
||||
id: String,
|
||||
version: String,
|
||||
base_dir: PathBuf,
|
||||
@ -82,7 +88,7 @@ impl GameDownloadAgent {
|
||||
version,
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
buckets: Mutex::new(Vec::new()),
|
||||
context_map: Mutex::new(HashMap::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
@ -90,7 +96,7 @@ impl GameDownloadAgent {
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
};
|
||||
|
||||
result.ensure_manifest_exists()?;
|
||||
result.ensure_manifest_exists().await?;
|
||||
|
||||
let required_space = result
|
||||
.manifest
|
||||
@ -100,8 +106,7 @@ impl GameDownloadAgent {
|
||||
.unwrap()
|
||||
.values()
|
||||
.map(|e| e.lengths.iter().sum::<usize>())
|
||||
.sum::<usize>()
|
||||
as u64;
|
||||
.sum::<usize>() as u64;
|
||||
|
||||
let available_space = get_disk_available(data_base_dir_path)? as u64;
|
||||
|
||||
@ -117,26 +122,25 @@ impl GameDownloadAgent {
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
let status = ApplicationTransientStatus::Downloading {
|
||||
version_name: self.version.clone(),
|
||||
};
|
||||
db_lock
|
||||
.applications
|
||||
.transient_statuses
|
||||
.insert(self.metadata(), status.clone());
|
||||
// Don't use GameStatusManager because this game isn't installed
|
||||
push_game_update(app_handle, &self.metadata().id, None, (None, Some(status)));
|
||||
|
||||
self.ensure_contexts()?;
|
||||
if !self.check_manifest_exists() {
|
||||
return Err(ApplicationDownloadError::NotInitialized);
|
||||
}
|
||||
|
||||
self.ensure_buckets()?;
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
db_lock.applications.transient_statuses.insert(
|
||||
self.metadata(),
|
||||
ApplicationTransientStatus::Downloading {
|
||||
version_name: self.version.clone(),
|
||||
},
|
||||
);
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&self.metadata().id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -147,9 +151,7 @@ impl GameDownloadAgent {
|
||||
|
||||
info!("beginning download for {}...", self.metadata().id);
|
||||
|
||||
let res = self
|
||||
.run()
|
||||
.map_err(|()| ApplicationDownloadError::DownloadError);
|
||||
let res = self.run().map_err(ApplicationDownloadError::Communication);
|
||||
|
||||
debug!(
|
||||
"{} took {}ms to download",
|
||||
@ -159,37 +161,43 @@ impl GameDownloadAgent {
|
||||
res
|
||||
}
|
||||
|
||||
pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
pub fn check_manifest_exists(&self) -> bool {
|
||||
self.manifest.lock().unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if self.manifest.lock().unwrap().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.download_manifest()
|
||||
self.download_manifest().await
|
||||
}
|
||||
|
||||
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let header = generate_authorization_header();
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
async fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let url = generate_url(
|
||||
&["/api/v1/client/game/manifest"],
|
||||
&[("id", &self.id), ("version", &self.version)],
|
||||
|f| f.header("Authorization", header),
|
||||
)
|
||||
.map_err(ApplicationDownloadError::Communication)?
|
||||
.send()
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
.map_err(ApplicationDownloadError::Communication)?;
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::ManifestDownloadFailed(
|
||||
response.status(),
|
||||
response.text().unwrap(),
|
||||
response.text().await.unwrap(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download: DropManifest = response.json().unwrap();
|
||||
let manifest_download: DropManifest = response.json().await.unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
@ -201,20 +209,23 @@ impl GameDownloadAgent {
|
||||
|
||||
// Sets it up for both download and validate
|
||||
fn setup_progress(&self) {
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
let buckets = self.buckets.lock().unwrap();
|
||||
|
||||
let length = contexts.len();
|
||||
let chunk_count = buckets.iter().map(|e| e.drops.len()).sum();
|
||||
|
||||
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
|
||||
let total_length = buckets
|
||||
.iter()
|
||||
.map(|bucket| bucket.drops.iter().map(|e| e.length).sum::<usize>())
|
||||
.sum();
|
||||
|
||||
self.progress.set_max(chunk_count);
|
||||
self.progress.set_size(length);
|
||||
self.progress.set_max(total_length);
|
||||
self.progress.set_size(chunk_count);
|
||||
self.progress.reset();
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if self.contexts.lock().unwrap().is_empty() {
|
||||
self.generate_contexts()?;
|
||||
pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if self.buckets.lock().unwrap().is_empty() {
|
||||
self.generate_buckets()?;
|
||||
}
|
||||
|
||||
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
|
||||
@ -222,14 +233,22 @@ impl GameDownloadAgent {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||
let game_id = self.id.clone();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let base_path = Path::new(&self.dropdata.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
let mut buckets = Vec::new();
|
||||
|
||||
let mut current_bucket = DownloadBucket {
|
||||
game_id: game_id.clone(),
|
||||
version: self.version.clone(),
|
||||
drops: Vec::new(),
|
||||
};
|
||||
let mut current_bucket_size = 0;
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
let path = base_path.join(Path::new(&raw_path));
|
||||
|
||||
@ -244,42 +263,79 @@ impl GameDownloadAgent {
|
||||
.truncate(false)
|
||||
.open(path.clone())
|
||||
.unwrap();
|
||||
let mut running_offset = 0;
|
||||
let mut file_running_offset = 0;
|
||||
|
||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||
contexts.push(DropDownloadContext {
|
||||
file_name: raw_path.to_string(),
|
||||
version: chunk.version_name.to_string(),
|
||||
offset: running_offset,
|
||||
index,
|
||||
game_id: game_id.to_string(),
|
||||
path: path.clone(),
|
||||
checksum: chunk.checksums[index].clone(),
|
||||
let drop = DownloadDrop {
|
||||
filename: raw_path.to_string(),
|
||||
start: file_running_offset,
|
||||
length: *length,
|
||||
checksum: chunk.checksums[index].clone(),
|
||||
permissions: chunk.permissions,
|
||||
});
|
||||
running_offset += *length as u64;
|
||||
path: path.clone(),
|
||||
index,
|
||||
};
|
||||
file_running_offset += *length;
|
||||
|
||||
if *length >= TARGET_BUCKET_SIZE {
|
||||
// They get their own bucket
|
||||
|
||||
buckets.push(DownloadBucket {
|
||||
game_id: game_id.clone(),
|
||||
version: self.version.clone(),
|
||||
drops: vec![drop],
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if current_bucket_size + *length >= TARGET_BUCKET_SIZE
|
||||
&& !current_bucket.drops.is_empty()
|
||||
{
|
||||
// Move current bucket into list and make a new one
|
||||
buckets.push(current_bucket);
|
||||
current_bucket = DownloadBucket {
|
||||
game_id: game_id.clone(),
|
||||
version: self.version.clone(),
|
||||
drops: Vec::new(),
|
||||
};
|
||||
current_bucket_size = 0;
|
||||
}
|
||||
|
||||
current_bucket.drops.push(drop);
|
||||
current_bucket_size += *length;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 && !already_exists {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
if file_running_offset > 0 && !already_exists {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, file_running_offset as u64);
|
||||
}
|
||||
}
|
||||
let existing_contexts = self.dropdata.get_completed_contexts();
|
||||
|
||||
if !current_bucket.drops.is_empty() {
|
||||
buckets.push(current_bucket);
|
||||
}
|
||||
|
||||
info!("buckets: {}", buckets.len());
|
||||
|
||||
let existing_contexts = self.dropdata.get_contexts();
|
||||
self.dropdata.set_contexts(
|
||||
&contexts
|
||||
&buckets
|
||||
.iter()
|
||||
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
|
||||
.flat_map(|x| x.drops.iter().map(|v| v.checksum.clone()))
|
||||
.map(|x| {
|
||||
let contains = existing_contexts.get(&x).unwrap_or(&false);
|
||||
(x, *contains)
|
||||
})
|
||||
.collect::<Vec<(String, bool)>>(),
|
||||
);
|
||||
|
||||
*self.contexts.lock().unwrap() = contexts;
|
||||
*self.buckets.lock().unwrap() = buckets;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(&self) -> Result<bool, ()> {
|
||||
fn run(&self) -> Result<bool, RemoteAccessError> {
|
||||
self.setup_progress();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
@ -295,78 +351,81 @@ impl GameDownloadAgent {
|
||||
let completed_contexts = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_contexts.clone();
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
let download_context = DROP_CLIENT_SYNC
|
||||
.post(generate_url(&["/api/v2/client/context"], &[]).unwrap())
|
||||
.json(&ManifestBody {
|
||||
game: self.id.clone(),
|
||||
version: self.version.clone(),
|
||||
})
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()?;
|
||||
|
||||
if download_context.status() != 200 {
|
||||
return Err(RemoteAccessError::InvalidResponse(download_context.json()?));
|
||||
}
|
||||
|
||||
let download_context = &download_context.json::<DownloadContext>()?;
|
||||
|
||||
info!("download context: {}", download_context.context);
|
||||
|
||||
let buckets = self.buckets.lock().unwrap();
|
||||
pool.scope(|scope| {
|
||||
let client = &DROP_CLIENT_SYNC.clone();
|
||||
let context_map = self.context_map.lock().unwrap();
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let client = client.clone();
|
||||
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||
for (index, bucket) in buckets.iter().enumerate() {
|
||||
let mut bucket = (*bucket).clone();
|
||||
let completed_contexts = completed_indexes_loop_arc.clone();
|
||||
|
||||
let progress = self.progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
|
||||
// If we've done this one already, skip it
|
||||
// Note to future DecDuck, DropData gets loaded into context_map
|
||||
if let Some(v) = context_map.get(&context.checksum)
|
||||
&& *v
|
||||
{
|
||||
progress_handle.skip(context.length);
|
||||
let todo_drops = bucket
|
||||
.drops
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
let todo = !*context_map.get(&e.checksum).unwrap_or(&false);
|
||||
if !todo {
|
||||
progress_handle.skip(e.length);
|
||||
}
|
||||
todo
|
||||
})
|
||||
.collect::<Vec<DownloadDrop>>();
|
||||
|
||||
if todo_drops.is_empty() {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
bucket.drops = todo_drops;
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let request = match make_request(
|
||||
&client,
|
||||
&["/api/v1/client/chunk"],
|
||||
&[
|
||||
("id", &context.game_id),
|
||||
("version", &context.version),
|
||||
("name", &context.file_name),
|
||||
("chunk", &context.index.to_string()),
|
||||
],
|
||||
|r| r,
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
sender
|
||||
.send(DownloadManagerSignal::Error(
|
||||
ApplicationDownloadError::Communication(e),
|
||||
))
|
||||
.unwrap();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
scope.spawn(move |_| {
|
||||
// 3 attempts
|
||||
for i in 0..RETRY_COUNT {
|
||||
let loop_progress_handle = progress_handle.clone();
|
||||
match download_game_chunk(
|
||||
context,
|
||||
match download_game_bucket(
|
||||
&bucket,
|
||||
download_context,
|
||||
&self.control_flag,
|
||||
loop_progress_handle,
|
||||
request.try_clone().unwrap(),
|
||||
) {
|
||||
Ok(true) => {
|
||||
completed_indexes.push(context.checksum.clone());
|
||||
for drop in bucket.drops {
|
||||
completed_contexts.push(drop.checksum);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Ok(false) => return,
|
||||
Err(e) => {
|
||||
warn!("game download agent error: {e}");
|
||||
|
||||
let retry = match &e {
|
||||
ApplicationDownloadError::Communication(
|
||||
_remote_access_error,
|
||||
) => true,
|
||||
ApplicationDownloadError::Checksum => true,
|
||||
ApplicationDownloadError::Lock => true,
|
||||
ApplicationDownloadError::IoError(_error_kind) => false,
|
||||
ApplicationDownloadError::DownloadError => false,
|
||||
ApplicationDownloadError::DiskFull(_, _) => false,
|
||||
};
|
||||
let retry = matches!(
|
||||
&e,
|
||||
ApplicationDownloadError::Communication(_)
|
||||
| ApplicationDownloadError::Checksum
|
||||
| ApplicationDownloadError::Lock
|
||||
);
|
||||
|
||||
if i == RETRY_COUNT - 1 || !retry {
|
||||
warn!("retry logic failed, not re-attempting.");
|
||||
@ -390,14 +449,14 @@ impl GameDownloadAgent {
|
||||
|
||||
context_map_lock.values().filter(|x| **x).count()
|
||||
};
|
||||
|
||||
let context_map_lock = self.context_map.lock().unwrap();
|
||||
let contexts = contexts
|
||||
let contexts = buckets
|
||||
.iter()
|
||||
.flat_map(|x| x.drops.iter().map(|e| e.checksum.clone()))
|
||||
.map(|x| {
|
||||
(
|
||||
x.checksum.clone(),
|
||||
context_map_lock.get(&x.checksum).copied().unwrap_or(false),
|
||||
)
|
||||
let completed = context_map_lock.get(&x).unwrap_or(&false);
|
||||
(x, *completed)
|
||||
})
|
||||
.collect::<Vec<(String, bool)>>();
|
||||
drop(context_map_lock);
|
||||
@ -408,10 +467,11 @@ impl GameDownloadAgent {
|
||||
// If there are any contexts left which are false
|
||||
if !contexts.iter().all(|x| x.1) {
|
||||
info!(
|
||||
"download agent for {} exited without completing ({}/{})",
|
||||
"download agent for {} exited without completing ({}/{}) ({} buckets)",
|
||||
self.id.clone(),
|
||||
completed_lock_len,
|
||||
contexts.len(),
|
||||
buckets.len()
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
@ -442,13 +502,15 @@ impl GameDownloadAgent {
|
||||
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
self.setup_validate(app_handle);
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
let buckets = self.buckets.lock().unwrap();
|
||||
let contexts: Vec<DropValidateContext> = buckets
|
||||
.clone()
|
||||
.into_iter()
|
||||
.flat_map(|e| -> Vec<DropValidateContext> { e.into() })
|
||||
.collect();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"validating game: {} with {} threads",
|
||||
self.dropdata.game_id, max_download_threads
|
||||
);
|
||||
info!("{} validation contexts", contexts.len());
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.build()
|
||||
@ -549,6 +611,13 @@ impl Downloadable for GameDownloadAgent {
|
||||
.applications
|
||||
.transient_statuses
|
||||
.remove(&self.metadata());
|
||||
|
||||
push_game_update(
|
||||
app_handle,
|
||||
&self.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&self.id, &handle),
|
||||
);
|
||||
}
|
||||
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
|
||||
@ -5,13 +5,15 @@ use crate::download_manager::util::progress_object::ProgressHandle;
|
||||
use crate::error::application_download_error::ApplicationDownloadError;
|
||||
use crate::error::drop_server_error::DropServerError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::DropDownloadContext;
|
||||
use crate::games::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop};
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use log::{debug, warn};
|
||||
use crate::remote::requests::generate_url;
|
||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||
use log::{info, warn};
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::{RequestBuilder, Response};
|
||||
use reqwest::blocking::Response;
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@ -21,21 +23,29 @@ use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
static MAX_PACKET_LENGTH: usize = 4096 * 4;
|
||||
|
||||
pub struct DropWriter<W: Write> {
|
||||
hasher: Context,
|
||||
destination: W,
|
||||
destination: BufWriter<W>,
|
||||
progress: ProgressHandle,
|
||||
}
|
||||
impl DropWriter<File> {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
let destination = OpenOptions::new().write(true).create(true).truncate(false).open(&path).unwrap();
|
||||
Self {
|
||||
destination,
|
||||
fn new(path: PathBuf, progress: ProgressHandle) -> Result<Self, io::Error> {
|
||||
let destination = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&path)?;
|
||||
Ok(Self {
|
||||
destination: BufWriter::with_capacity(1024 * 1024, destination),
|
||||
hasher: Context::new(),
|
||||
}
|
||||
progress,
|
||||
})
|
||||
}
|
||||
|
||||
fn finish(mut self) -> io::Result<Digest> {
|
||||
self.flush().unwrap();
|
||||
self.flush()?;
|
||||
Ok(self.hasher.compute())
|
||||
}
|
||||
}
|
||||
@ -45,7 +55,10 @@ impl Write for DropWriter<File> {
|
||||
self.hasher
|
||||
.write_all(buf)
|
||||
.map_err(|e| io::Error::other(format!("Unable to write to hasher: {e}")))?;
|
||||
self.destination.write(buf)
|
||||
let bytes_written = self.destination.write(buf)?;
|
||||
self.progress.add(bytes_written);
|
||||
|
||||
Ok(bytes_written)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
@ -62,91 +75,103 @@ impl Seek for DropWriter<File> {
|
||||
|
||||
pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
||||
pub source: R,
|
||||
pub destination: DropWriter<W>,
|
||||
pub drops: Vec<DownloadDrop>,
|
||||
pub destination: Vec<DropWriter<W>>,
|
||||
pub control_flag: &'a DownloadThreadControl,
|
||||
pub progress: ProgressHandle,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||
fn new(
|
||||
source: Response,
|
||||
destination: DropWriter<File>,
|
||||
drops: Vec<DownloadDrop>,
|
||||
control_flag: &'a DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
size: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
) -> Result<Self, io::Error> {
|
||||
Ok(Self {
|
||||
source,
|
||||
destination,
|
||||
destination: drops
|
||||
.iter()
|
||||
.map(|drop| DropWriter::new(drop.path.clone(), progress.clone()))
|
||||
.try_collect()?,
|
||||
drops,
|
||||
control_flag,
|
||||
progress,
|
||||
size,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||
let copy_buf_size = 512;
|
||||
let mut copy_buf = vec![0; copy_buf_size];
|
||||
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||
let mut copy_buffer = [0u8; MAX_PACKET_LENGTH];
|
||||
for (index, drop) in self.drops.iter().enumerate() {
|
||||
let destination = self
|
||||
.destination
|
||||
.get_mut(index)
|
||||
.ok_or(io::Error::other("no destination"))
|
||||
.unwrap();
|
||||
let mut remaining = drop.length;
|
||||
if drop.start != 0 {
|
||||
destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?;
|
||||
}
|
||||
loop {
|
||||
let size = MAX_PACKET_LENGTH.min(remaining);
|
||||
self.source.read_exact(&mut copy_buffer[0..size])?;
|
||||
remaining -= size;
|
||||
|
||||
destination.write_all(©_buffer[0..size])?;
|
||||
|
||||
if remaining == 0 {
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
let mut current_size = 0;
|
||||
loop {
|
||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
buf_writer.flush()?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut bytes_read = self.source.read(&mut copy_buf)?;
|
||||
current_size += bytes_read;
|
||||
|
||||
if current_size > self.size {
|
||||
let over = current_size - self.size;
|
||||
warn!("server sent too many bytes... {over} over");
|
||||
bytes_read -= over;
|
||||
current_size = self.size;
|
||||
}
|
||||
|
||||
buf_writer.write_all(©_buf[0..bytes_read])?;
|
||||
self.progress.add(bytes_read);
|
||||
|
||||
if current_size >= self.size {
|
||||
debug!(
|
||||
"finished with final size of {} vs {}",
|
||||
current_size, self.size
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf_writer.flush()?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Digest, io::Error> {
|
||||
let checksum = self.destination.finish()?;
|
||||
Ok(checksum)
|
||||
fn finish(self) -> Result<Vec<Digest>, io::Error> {
|
||||
let checksums = self
|
||||
.destination
|
||||
.into_iter()
|
||||
.map(|e| e.finish())
|
||||
.try_collect()?;
|
||||
Ok(checksums)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download_game_chunk(
|
||||
ctx: &DropDownloadContext,
|
||||
pub fn download_game_bucket(
|
||||
bucket: &DownloadBucket,
|
||||
ctx: &DownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
request: RequestBuilder,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
let response = request
|
||||
.header("Authorization", generate_authorization_header())
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let url = generate_url(&["/api/v2/client/chunk"], &[])
|
||||
.map_err(ApplicationDownloadError::Communication)?;
|
||||
|
||||
let body = ChunkBody::create(ctx, &bucket.drops);
|
||||
|
||||
let response = DROP_CLIENT_SYNC
|
||||
.post(url)
|
||||
.json(&body)
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
debug!("chunk request got status code: {}", response.status());
|
||||
let raw_res = response.text().unwrap();
|
||||
info!("chunk request got status code: {}", response.status());
|
||||
let raw_res = response.text().map_err(|e| {
|
||||
ApplicationDownloadError::Communication(RemoteAccessError::FetchError(e.into()))
|
||||
})?;
|
||||
info!("{}", raw_res);
|
||||
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(err),
|
||||
@ -157,30 +182,35 @@ pub fn download_game_chunk(
|
||||
));
|
||||
}
|
||||
|
||||
let mut destination = DropWriter::new(ctx.path.clone());
|
||||
let lengths = response
|
||||
.headers()
|
||||
.get("Content-Lengths")
|
||||
.ok_or(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()),
|
||||
))?
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
if ctx.offset != 0 {
|
||||
destination
|
||||
.seek(SeekFrom::Start(ctx.offset))
|
||||
.expect("Failed to seek to file offset");
|
||||
}
|
||||
|
||||
let content_length = response.content_length();
|
||||
if content_length.is_none() {
|
||||
warn!("recieved 0 length content from server");
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(response.json().unwrap()),
|
||||
));
|
||||
}
|
||||
|
||||
let length = content_length.unwrap().try_into().unwrap();
|
||||
|
||||
if length != ctx.length {
|
||||
return Err(ApplicationDownloadError::DownloadError);
|
||||
for (i, raw_length) in lengths.split(",").enumerate() {
|
||||
let length = raw_length.parse::<usize>().unwrap_or(0);
|
||||
let Some(drop) = bucket.drops.get(i) else {
|
||||
warn!("invalid number of Content-Lengths recieved: {}, {}", i, lengths);
|
||||
return Err(ApplicationDownloadError::DownloadError);
|
||||
};
|
||||
if drop.length != length {
|
||||
warn!(
|
||||
"for {}, expected {}, got {} ({})",
|
||||
drop.filename, drop.length, raw_length, length
|
||||
);
|
||||
return Err(ApplicationDownloadError::DownloadError);
|
||||
}
|
||||
}
|
||||
|
||||
let mut pipeline =
|
||||
DropDownloadPipeline::new(response, destination, control_flag, progress, length);
|
||||
DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress)
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
|
||||
let completed = pipeline
|
||||
.copy()
|
||||
@ -192,23 +222,23 @@ pub fn download_game_chunk(
|
||||
// If we complete the file, set the permissions (if on Linux)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let permissions = Permissions::from_mode(ctx.permissions);
|
||||
set_permissions(ctx.path.clone(), permissions).unwrap();
|
||||
for drop in bucket.drops.iter() {
|
||||
let permissions = Permissions::from_mode(drop.permissions);
|
||||
set_permissions(drop.path.clone(), permissions)
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
}
|
||||
}
|
||||
|
||||
let checksum = pipeline
|
||||
let checksums = pipeline
|
||||
.finish()
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
|
||||
let res = hex::encode(checksum.0);
|
||||
if res != ctx.checksum {
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
for (index, drop) in bucket.drops.iter().enumerate() {
|
||||
let res = hex::encode(**checksums.get(index).unwrap());
|
||||
if res != drop.checksum {
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Successfully finished download #{}, copied {} bytes",
|
||||
ctx.checksum, length
|
||||
);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@ -76,14 +76,6 @@ impl DropData {
|
||||
pub fn set_context(&self, context: String, state: bool) {
|
||||
self.contexts.lock().unwrap().entry(context).insert_entry(state);
|
||||
}
|
||||
pub fn get_completed_contexts(&self) -> Vec<String> {
|
||||
self.contexts
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|x| if *x.1 { Some(x.0.clone()) } else { None })
|
||||
.collect()
|
||||
}
|
||||
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
||||
self.contexts.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
@ -2,6 +2,65 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
// Drops go in buckets
|
||||
pub struct DownloadDrop {
|
||||
pub index: usize,
|
||||
pub filename: String,
|
||||
pub path: PathBuf,
|
||||
pub start: usize,
|
||||
pub length: usize,
|
||||
pub checksum: String,
|
||||
pub permissions: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DownloadBucket {
|
||||
pub game_id: String,
|
||||
pub version: String,
|
||||
pub drops: Vec<DownloadDrop>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DownloadContext {
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChunkBodyFile {
|
||||
filename: String,
|
||||
chunk_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChunkBody {
|
||||
pub context: String,
|
||||
pub files: Vec<ChunkBodyFile>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ManifestBody {
|
||||
pub game: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl ChunkBody {
|
||||
pub fn create(context: &DownloadContext, drops: &[DownloadDrop]) -> ChunkBody {
|
||||
Self {
|
||||
context: context.context.clone(),
|
||||
files: drops
|
||||
.iter()
|
||||
.map(|e| ChunkBodyFile {
|
||||
filename: e.filename.clone(),
|
||||
chunk_index: e.index,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DropManifest = HashMap<String, DropChunk>;
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -14,14 +73,26 @@ pub struct DropChunk {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DropDownloadContext {
|
||||
pub file_name: String,
|
||||
pub version: String,
|
||||
pub struct DropValidateContext {
|
||||
pub index: usize,
|
||||
pub offset: u64,
|
||||
pub game_id: String,
|
||||
pub offset: usize,
|
||||
pub path: PathBuf,
|
||||
pub checksum: String,
|
||||
pub length: usize,
|
||||
pub permissions: u32,
|
||||
}
|
||||
|
||||
impl From<DownloadBucket> for Vec<DropValidateContext> {
|
||||
fn from(value: DownloadBucket) -> Self {
|
||||
value
|
||||
.drops
|
||||
.into_iter()
|
||||
.map(|e| DropValidateContext {
|
||||
index: e.index,
|
||||
offset: e.start,
|
||||
path: e.path,
|
||||
checksum: e.checksum,
|
||||
length: e.length,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,24 +7,22 @@ use log::debug;
|
||||
use md5::Context;
|
||||
|
||||
use crate::{
|
||||
download_manager::
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::ProgressHandle,
|
||||
}
|
||||
,
|
||||
download_manager::util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::ProgressHandle,
|
||||
},
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
games::downloads::manifest::DropDownloadContext,
|
||||
games::downloads::manifest::DropValidateContext,
|
||||
};
|
||||
|
||||
pub fn validate_game_chunk(
|
||||
ctx: &DropDownloadContext,
|
||||
ctx: &DropValidateContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
debug!(
|
||||
"Starting chunk validation {}, {}, {} #{}",
|
||||
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
|
||||
ctx.path.display(), ctx.index, ctx.offset, ctx.checksum
|
||||
);
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
@ -38,7 +36,7 @@ pub fn validate_game_chunk(
|
||||
|
||||
if ctx.offset != 0 {
|
||||
source
|
||||
.seek(SeekFrom::Start(ctx.offset))
|
||||
.seek(SeekFrom::Start(ctx.offset.try_into().unwrap()))
|
||||
.expect("Failed to seek to file offset");
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,8 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use crate::remote::cache::cache_object_db;
|
||||
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::remote::requests::generate_url;
|
||||
use crate::remote::utils::DROP_CLIENT_ASYNC;
|
||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||
use bitcode::{Decode, Encode};
|
||||
|
||||
@ -76,24 +77,24 @@ pub struct StatsUpdateEvent {
|
||||
pub time: usize,
|
||||
}
|
||||
|
||||
pub fn fetch_library_logic(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
pub async fn fetch_library_logic(
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})?
|
||||
.send()?;
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
let err = response.json().await.unwrap();
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let mut games: Vec<Game> = response.json()?;
|
||||
let mut games: Vec<Game> = response.json().await?;
|
||||
|
||||
let mut handle = state.lock().unwrap();
|
||||
|
||||
@ -135,73 +136,89 @@ pub fn fetch_library_logic(
|
||||
|
||||
Ok(games)
|
||||
}
|
||||
pub fn fetch_library_logic_offline(
|
||||
_state: tauri::State<'_, Mutex<AppState>>,
|
||||
pub async fn fetch_library_logic_offline(
|
||||
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let mut games: Vec<Game> = get_cached_object("library")?;
|
||||
|
||||
let db_handle = borrow_db_checked();
|
||||
|
||||
games.retain(|game| {
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.contains_key(&game.id)
|
||||
matches!(
|
||||
&db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game.id)
|
||||
.unwrap_or(&GameDownloadStatus::Remote {}),
|
||||
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
|
||||
)
|
||||
});
|
||||
|
||||
Ok(games)
|
||||
}
|
||||
pub fn fetch_game_logic(
|
||||
pub async fn fetch_game_logic(
|
||||
id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let mut state_handle = state.lock().unwrap();
|
||||
let version = {
|
||||
let state_handle = state.lock().unwrap();
|
||||
|
||||
let db_lock = borrow_db_checked();
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
|
||||
let game = state_handle.games.get(&id);
|
||||
if let Some(game) = game {
|
||||
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
status,
|
||||
version,
|
||||
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
|
||||
cache_object_db(&id, game, &db_lock)?;
|
||||
let game = state_handle.games.get(&id);
|
||||
if let Some(game) = game {
|
||||
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
drop(db_lock);
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
status,
|
||||
version,
|
||||
};
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send()?;
|
||||
cache_object_db(&id, game, &db_lock)?;
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
version
|
||||
};
|
||||
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() == 404 {
|
||||
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
|
||||
if let Ok(fetch_data) = offline_fetch {
|
||||
return Ok(fetch_data);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::GameNotFound(id));
|
||||
}
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
let err = response.json().await.unwrap();
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let game: Game = response.json()?;
|
||||
let game: Game = response.json().await?;
|
||||
|
||||
let mut state_handle = state.lock().unwrap();
|
||||
state_handle.games.insert(id.clone(), game.clone());
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
@ -227,24 +244,20 @@ pub fn fetch_game_logic(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn fetch_game_logic_offline(
|
||||
pub async fn fetch_game_logic_offline(
|
||||
id: String,
|
||||
_state: tauri::State<'_, Mutex<AppState>>,
|
||||
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let db_handle = borrow_db_checked();
|
||||
let metadata_option = db_handle.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => Some(
|
||||
db_handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.unwrap()
|
||||
.get(metadata.version.as_ref().unwrap())
|
||||
.unwrap()
|
||||
.clone(),
|
||||
),
|
||||
Some(metadata) => db_handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
@ -259,27 +272,26 @@ pub fn fetch_game_logic_offline(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_game_verion_options_logic(
|
||||
pub async fn fetch_game_version_options_logic(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/versions"],
|
||||
&[("id", &game_id)],
|
||||
|r| r.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().unwrap();
|
||||
let err = response.json().await.unwrap();
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let data: Vec<GameVersion> = response.json()?;
|
||||
let data: Vec<GameVersion> = response.json().await?;
|
||||
|
||||
let state_lock = state.lock().unwrap();
|
||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
@ -440,19 +452,18 @@ pub fn on_game_complete(
|
||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||
}
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
let response = generate_url(
|
||||
&["/api/v1/client/game/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
|f| f.header("Authorization", header),
|
||||
)?
|
||||
.send()?;
|
||||
)?;
|
||||
let response = client
|
||||
.get(response)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()?;
|
||||
|
||||
let game_version: GameVersion = response.json()?;
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
#![deny(unused_must_use)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(duration_constructors)]
|
||||
#![feature(duration_millis_float)]
|
||||
#![feature(iterator_try_collect)]
|
||||
#![deny(clippy::all)]
|
||||
|
||||
mod database;
|
||||
@ -38,7 +41,7 @@ use games::collections::commands::{
|
||||
fetch_collection, fetch_collections,
|
||||
};
|
||||
use games::commands::{
|
||||
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
|
||||
fetch_game, fetch_game_status, fetch_game_version_options, fetch_library, uninstall_game,
|
||||
};
|
||||
use games::downloads::commands::download_game;
|
||||
use games::library::{Game, update_game_configuration};
|
||||
@ -134,7 +137,7 @@ pub struct AppState<'a> {
|
||||
compat_info: Option<CompatInfo>,
|
||||
}
|
||||
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||
@ -189,7 +192,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
debug!("database is set up");
|
||||
|
||||
// TODO: Account for possible failure
|
||||
let (app_status, user) = auth::setup();
|
||||
let (app_status, user) = auth::setup().await;
|
||||
|
||||
let db_handle = borrow_db_checked();
|
||||
let mut missing_games = Vec::new();
|
||||
@ -316,7 +319,7 @@ pub fn run() {
|
||||
delete_download_dir,
|
||||
fetch_download_dir_stats,
|
||||
fetch_game_status,
|
||||
fetch_game_verion_options,
|
||||
fetch_game_version_options,
|
||||
update_game_configuration,
|
||||
// Collections
|
||||
fetch_collections,
|
||||
@ -348,92 +351,99 @@ pub fn run() {
|
||||
))
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
let state = setup(handle);
|
||||
debug!("initialized drop client");
|
||||
app.manage(Mutex::new(state));
|
||||
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
let _ = app.deep_link().register_all();
|
||||
debug!("registered all pre-defined deep links");
|
||||
}
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let state = setup(handle).await;
|
||||
info!("initialized drop client");
|
||||
app.manage(Mutex::new(state));
|
||||
|
||||
let handle = app.handle().clone();
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
let _ = app.deep_link().register_all();
|
||||
debug!("registered all pre-defined deep links");
|
||||
}
|
||||
|
||||
let _main_window = tauri::WebviewWindowBuilder::new(
|
||||
&handle,
|
||||
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
|
||||
tauri::WebviewUrl::App("main".into()),
|
||||
)
|
||||
.title("Drop Desktop App")
|
||||
.min_inner_size(1000.0, 500.0)
|
||||
.inner_size(1536.0, 864.0)
|
||||
.decorations(false)
|
||||
.shadow(false)
|
||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
||||
.build()
|
||||
.unwrap();
|
||||
let handle = app.handle().clone();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
debug!("handling drop:// url");
|
||||
let binding = event.urls();
|
||||
let url = binding.first().unwrap();
|
||||
if url.host_str().unwrap() == "handshake" {
|
||||
recieve_handshake(handle.clone(), url.path().to_string());
|
||||
let _main_window = tauri::WebviewWindowBuilder::new(
|
||||
&handle,
|
||||
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
|
||||
tauri::WebviewUrl::App("main".into()),
|
||||
)
|
||||
.title("Drop Desktop App")
|
||||
.min_inner_size(1000.0, 500.0)
|
||||
.inner_size(1536.0, 864.0)
|
||||
.decorations(false)
|
||||
.shadow(false)
|
||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
debug!("handling drop:// url");
|
||||
let binding = event.urls();
|
||||
let url = binding.first().unwrap();
|
||||
if url.host_str().unwrap() == "handshake" {
|
||||
tauri::async_runtime::spawn(recieve_handshake(
|
||||
handle.clone(),
|
||||
url.path().to_string(),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
let menu = Menu::with_items(
|
||||
app,
|
||||
&[
|
||||
&MenuItem::with_id(app, "open", "Open", true, None::<&str>).unwrap(),
|
||||
&PredefinedMenuItem::separator(app).unwrap(),
|
||||
/*
|
||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
*/
|
||||
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
run_on_tray(|| {
|
||||
TrayIconBuilder::new()
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"open" => {
|
||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||
}
|
||||
"quit" => {
|
||||
cleanup_and_exit(app, &app.state());
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("menu event not handled: {:?}", event.id);
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.expect("error while setting up tray menu");
|
||||
});
|
||||
|
||||
{
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
original.canonicalize().unwrap().to_string_lossy()
|
||||
);
|
||||
app.dialog()
|
||||
.message(
|
||||
"Database corrupted. A copy has been saved at: ".to_string()
|
||||
+ original.to_str().unwrap(),
|
||||
)
|
||||
.title("Database corrupted")
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let menu = Menu::with_items(
|
||||
app,
|
||||
&[
|
||||
&MenuItem::with_id(app, "open", "Open", true, None::<&str>)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
/*
|
||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
*/
|
||||
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
run_on_tray(|| {
|
||||
TrayIconBuilder::new()
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"open" => {
|
||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||
}
|
||||
"quit" => {
|
||||
cleanup_and_exit(app, &app.state());
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("menu event not handled: {:?}", event.id);
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.expect("error while setting up tray menu");
|
||||
});
|
||||
|
||||
{
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
original.canonicalize().unwrap().to_string_lossy()
|
||||
);
|
||||
app.dialog()
|
||||
.message(
|
||||
"Database corrupted. A copy has been saved at: ".to_string()
|
||||
+ original.to_str().unwrap(),
|
||||
)
|
||||
.title("Database corrupted")
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
|
||||
@ -441,15 +451,21 @@ pub fn run() {
|
||||
fetch_object(request, responder).await;
|
||||
});
|
||||
})
|
||||
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| {
|
||||
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
|
||||
offline!(
|
||||
state,
|
||||
handle_server_proto,
|
||||
handle_server_proto_offline,
|
||||
request,
|
||||
responder
|
||||
);
|
||||
.register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let state = ctx
|
||||
.app_handle()
|
||||
.state::<tauri::State<'_, Mutex<AppState>>>();
|
||||
|
||||
offline!(
|
||||
state,
|
||||
handle_server_proto,
|
||||
handle_server_proto_offline,
|
||||
request,
|
||||
responder
|
||||
)
|
||||
.await;
|
||||
});
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
|
||||
@ -12,12 +12,12 @@ use crate::{
|
||||
database::{
|
||||
db::{borrow_db_checked, borrow_db_mut_checked},
|
||||
models::data::DatabaseAuth,
|
||||
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::utils::DROP_CLIENT_SYNC, AppState, AppStatus, User
|
||||
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::{requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User
|
||||
};
|
||||
|
||||
use super::{
|
||||
cache::{cache_object, get_cached_object},
|
||||
requests::make_request,
|
||||
requests::generate_url,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -61,16 +61,10 @@ pub fn generate_authorization_header() -> String {
|
||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||
}
|
||||
|
||||
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})?
|
||||
.send()?;
|
||||
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json()?;
|
||||
let err: DropServerError = response.json().await?;
|
||||
warn!("{err:?}");
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
@ -80,10 +74,13 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
response.json::<User>().map_err(std::convert::Into::into)
|
||||
response
|
||||
.json::<User>()
|
||||
.await
|
||||
.map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||
let path_chunks: Vec<&str> = path.split('/').collect();
|
||||
if path_chunks.len() != 3 {
|
||||
app.emit("auth/failed", ()).unwrap();
|
||||
@ -105,13 +102,13 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
};
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client.post(endpoint).json(&body).send()?;
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = client.post(endpoint).json(&body).send().await?;
|
||||
debug!("handshake responsded with {}", response.status().as_u16());
|
||||
if !response.status().is_success() {
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json()?));
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
||||
}
|
||||
let response_struct: HandshakeResponse = response.json()?;
|
||||
let response_struct: HandshakeResponse = response.json().await?;
|
||||
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
@ -129,9 +126,10 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
token.text().unwrap()
|
||||
token.text().await.unwrap()
|
||||
};
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
@ -141,11 +139,11 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
||||
// Tell the app we're processing
|
||||
app.emit("auth/processing", ()).unwrap();
|
||||
|
||||
let handshake_result = recieve_handshake_logic(&app, path);
|
||||
let handshake_result = recieve_handshake_logic(&app, path).await;
|
||||
if let Err(e) = handshake_result {
|
||||
warn!("error with authentication: {e}");
|
||||
app.emit("auth/failed", e.to_string()).unwrap();
|
||||
@ -153,9 +151,10 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
}
|
||||
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut state_lock = app_state.lock().unwrap();
|
||||
|
||||
let (app_status, user) = setup();
|
||||
let (app_status, user) = setup().await;
|
||||
|
||||
let mut state_lock = app_state.lock().unwrap();
|
||||
|
||||
state_lock.status = app_status;
|
||||
state_lock.user = user;
|
||||
@ -199,13 +198,14 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn setup() -> (AppStatus, Option<User>) {
|
||||
let data = borrow_db_checked();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
pub async fn setup() -> (AppStatus, Option<User>) {
|
||||
let auth = {
|
||||
let data = borrow_db_checked();
|
||||
data.auth.clone()
|
||||
};
|
||||
|
||||
if auth.is_some() {
|
||||
let user_result = match fetch_user() {
|
||||
let user_result = match fetch_user().await {
|
||||
Ok(data) => data,
|
||||
Err(RemoteAccessError::FetchError(_)) => {
|
||||
let user = get_cached_object::<User>("user").unwrap();
|
||||
|
||||
@ -11,16 +11,16 @@ use crate::{
|
||||
};
|
||||
use bitcode::{Decode, DecodeOwned, Encode};
|
||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||
use log::debug;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! offline {
|
||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||
|
||||
if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
||||
$func2( $( $arg ), *)
|
||||
async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
||||
$func2( $( $arg ), *).await
|
||||
} else {
|
||||
$func1( $( $arg ), *)
|
||||
$func1( $( $arg ), *).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,18 +68,9 @@ pub fn get_cached_object_db<D: DecodeOwned>(
|
||||
key: &str,
|
||||
db: &Database,
|
||||
) -> Result<D, RemoteAccessError> {
|
||||
let start = SystemTime::now();
|
||||
let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
|
||||
let read = start.elapsed().unwrap();
|
||||
let data =
|
||||
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
|
||||
let decode = start.elapsed().unwrap();
|
||||
debug!(
|
||||
"cache object took: r:{}, d:{}, b:{}",
|
||||
read.as_millis(),
|
||||
read.abs_diff(decode).as_millis(),
|
||||
bytes.len()
|
||||
);
|
||||
Ok(data)
|
||||
}
|
||||
#[derive(Encode, Decode)]
|
||||
|
||||
@ -8,11 +8,14 @@ use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked}, error::remote_access_error::RemoteAccessError, remote::{
|
||||
AppState, AppStatus,
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
remote::{
|
||||
auth::generate_authorization_header,
|
||||
requests::make_request,
|
||||
requests::generate_url,
|
||||
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
|
||||
}, AppState, AppStatus
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -45,10 +48,11 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
#[tauri::command]
|
||||
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
||||
let _drop_url = gen_drop_url(path.clone())?;
|
||||
let req = make_request(&DROP_CLIENT_SYNC, &[&path], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send();
|
||||
let req = generate_url(&[&path], &[])?;
|
||||
let req = DROP_CLIENT_SYNC
|
||||
.get(req)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send();
|
||||
|
||||
match req {
|
||||
Ok(data) => {
|
||||
@ -83,13 +87,15 @@ pub fn sign_out(app: AppHandle) {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
let (app_status, user) = setup();
|
||||
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> {
|
||||
let (app_status, user) = setup().await;
|
||||
|
||||
let mut guard = state.lock().unwrap();
|
||||
guard.status = app_status;
|
||||
guard.user = user;
|
||||
drop(guard);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -145,9 +151,7 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
||||
match response.response_type.as_str() {
|
||||
"token" => {
|
||||
let recieve_app = app.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
manual_recieve_handshake(recieve_app, response.value);
|
||||
});
|
||||
manual_recieve_handshake(recieve_app, response.value).await.unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
||||
@ -171,6 +175,8 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||
recieve_handshake(app, format!("handshake/{token}"));
|
||||
pub async fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), ()> {
|
||||
recieve_handshake(app, format!("handshake/{token}")).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
use reqwest::blocking::{Client, RequestBuilder};
|
||||
use url::Url;
|
||||
|
||||
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
|
||||
use crate::{
|
||||
DB,
|
||||
database::db::DatabaseImpls,
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
remote::{auth::generate_authorization_header, utils::DROP_CLIENT_ASYNC},
|
||||
};
|
||||
|
||||
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
|
||||
client: &Client,
|
||||
pub fn generate_url<T: AsRef<str>>(
|
||||
path_components: &[T],
|
||||
query: &[(T, T)],
|
||||
f: F,
|
||||
) -> Result<RequestBuilder, RemoteAccessError> {
|
||||
) -> Result<Url, RemoteAccessError> {
|
||||
let mut base_url = DB.fetch_base_url();
|
||||
for endpoint in path_components {
|
||||
base_url = base_url.join(endpoint.as_ref())?;
|
||||
@ -18,6 +21,13 @@ pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
|
||||
queries.append_pair(param.as_ref(), val.as_ref());
|
||||
}
|
||||
}
|
||||
let response = client.get(base_url);
|
||||
Ok(f(response))
|
||||
Ok(base_url)
|
||||
}
|
||||
|
||||
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, reqwest::Error> {
|
||||
DROP_CLIENT_ASYNC
|
||||
.get(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ use tauri::UriSchemeResponder;
|
||||
|
||||
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC};
|
||||
|
||||
pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
let four_oh_four = Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Vec::new())
|
||||
@ -13,7 +13,7 @@ pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSch
|
||||
responder.respond(four_oh_four);
|
||||
}
|
||||
|
||||
pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
let db_handle = borrow_db_checked();
|
||||
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
||||
Some(e) => e,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "Drop Desktop Client",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"identifier": "dev.drop.client",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn --cwd main dev --port 1432",
|
||||
|
||||
Reference in New Issue
Block a user