mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-10 04:22:13 +10:00
Download fixes (#63)
* refactor: Rename StoredManifest to DropData Signed-off-by: quexeky <git@quexeky.dev> * fix: Downloads when resuming would truncate files which had not been finished Signed-off-by: quexeky <git@quexeky.dev> * chore: Didn't import debug macro Signed-off-by: quexeky <git@quexeky.dev> * fix: Download chunks with wrong indexes Migrated to using checksums as indexes instead Signed-off-by: quexeky <git@quexeky.dev> * feat: Resume download button Also added DBWrite and DBRead structs to make database management easier Signed-off-by: quexeky <git@quexeky.dev> * feat: Download resuming Signed-off-by: quexeky <git@quexeky.dev> * feat: Resume button and PartiallyInstalled status Signed-off-by: quexeky <git@quexeky.dev> * feat: Download validation Signed-off-by: quexeky <git@quexeky.dev> * chore: Ran cargo fix & cargo fmt Signed-off-by: quexeky <git@quexeky.dev> * fix: download validation, installs, etc * chore: version bump --------- Signed-off-by: quexeky <git@quexeky.dev> Co-authored-by: quexeky <git@quexeky.dev>
This commit is contained in:
@ -1,75 +1,49 @@
|
||||
<template>
|
||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||
<div class="inline-flex divide-x divide-zinc-900">
|
||||
<button
|
||||
type="button"
|
||||
@click="() => buttonActions[props.status.type]()"
|
||||
:class="[
|
||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
||||
styles[props.status.type],
|
||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="buttonIcons[props.status.type]"
|
||||
class="-mr-0.5 size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
]">
|
||||
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
|
||||
{{ buttonNames[props.status.type] }}
|
||||
</button>
|
||||
<Menu
|
||||
v-if="showDropdown"
|
||||
as="div"
|
||||
class="relative inline-block text-left grow"
|
||||
>
|
||||
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
|
||||
<div class="h-full">
|
||||
<MenuButton
|
||||
:class="[
|
||||
<MenuButton :class="[
|
||||
styles[props.status.type],
|
||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<MenuItems
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
|
||||
>
|
||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
|
||||
<div class="py-1">
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('options')"
|
||||
:class="[
|
||||
<button @click="() => emit('options')" :class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
Options
|
||||
<Cog6ToothIcon class="size-5" />
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ active }">
|
||||
<button
|
||||
@click="() => emit('uninstall')"
|
||||
:class="[
|
||||
<button @click="() => emit('uninstall')" :class="[
|
||||
active
|
||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||
: 'text-zinc-400',
|
||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
Uninstall
|
||||
<TrashIcon class="size-5" />
|
||||
</button>
|
||||
@ -103,12 +77,14 @@ const emit = defineEmits<{
|
||||
(e: "uninstall"): void;
|
||||
(e: "kill"): void;
|
||||
(e: "options"): void;
|
||||
(e: "resume"): void
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(
|
||||
() =>
|
||||
props.status.type === GameStatusEnum.Installed ||
|
||||
props.status.type === GameStatusEnum.SetupRequired
|
||||
props.status.type === GameStatusEnum.SetupRequired ||
|
||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
||||
);
|
||||
|
||||
const styles: { [key in GameStatusEnum]: string } = {
|
||||
@ -128,6 +104,8 @@ const styles: { [key in GameStatusEnum]: string } = {
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.Running]:
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[GameStatusEnum.PartiallyInstalled]:
|
||||
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
|
||||
};
|
||||
|
||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
@ -139,6 +117,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Updating]: "Updating",
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||
[GameStatusEnum.Running]: "Stop",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Resume"
|
||||
};
|
||||
|
||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
@ -150,6 +129,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||
[GameStatusEnum.Running]: PlayIcon,
|
||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
|
||||
};
|
||||
|
||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
@ -159,7 +139,8 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||
[GameStatusEnum.SetupRequired]: () => emit("launch"),
|
||||
[GameStatusEnum.Installed]: () => emit("launch"),
|
||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||
[GameStatusEnum.Uninstalling]: () => {},
|
||||
[GameStatusEnum.Uninstalling]: () => { },
|
||||
[GameStatusEnum.Running]: () => emit("kill"),
|
||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -76,6 +76,7 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Updating]: "text-blue-500",
|
||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-600"
|
||||
};
|
||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Remote]: "Not installed",
|
||||
@ -86,6 +87,7 @@ const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||
[GameStatusEnum.Running]: "Running",
|
||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed"
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "drop-app",
|
||||
"private": true,
|
||||
"version": "0.3.0-rc-4",
|
||||
"version": "0.3.0-rc-5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
@uninstall="() => uninstall()"
|
||||
@kill="() => kill()"
|
||||
@options="() => (configureModalOpen = true)"
|
||||
@resume="() => resumeDownload()"
|
||||
:status="status"
|
||||
/>
|
||||
<a
|
||||
@ -495,6 +496,7 @@ const currentImageIndex = ref(0);
|
||||
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
@ -532,6 +534,15 @@ async function install() {
|
||||
installLoading.value = false;
|
||||
}
|
||||
|
||||
async function resumeDownload() {
|
||||
try {
|
||||
await invoke("resume_download", { gameId: game.value.id })
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
try {
|
||||
await invoke("launch_game", { id: game.value.id });
|
||||
|
||||
@ -91,7 +91,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
|
||||
import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
||||
|
||||
// const actionNames = {
|
||||
// [GameStatusEnum.Downloading]: "downloading",
|
||||
// [GameStatusEnum.Verifying]: "verifying",
|
||||
// }
|
||||
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
window.addEventListener("resize", (event) => {
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1247,7 +1247,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "drop-app"
|
||||
version = "0.3.0-rc-4"
|
||||
version = "0.3.0-rc-5"
|
||||
dependencies = [
|
||||
"atomic-instant-full",
|
||||
"boxcar",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.0-rc-4"
|
||||
version = "0.3.0-rc-5"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2021"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use log::debug;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
@ -17,7 +17,6 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -6,10 +6,12 @@ use std::{
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
|
||||
use crate::{
|
||||
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
|
||||
};
|
||||
|
||||
use super::{
|
||||
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
|
||||
db::{borrow_db_checked, DATA_ROOT_DIR},
|
||||
debug::SystemData,
|
||||
models::data::Settings,
|
||||
};
|
||||
@ -26,8 +28,6 @@ pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||
pub fn delete_download_dir(index: usize) {
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
lock.applications.install_dirs.remove(index);
|
||||
drop(lock);
|
||||
save_db();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -58,7 +58,6 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
|
||||
}
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -72,8 +71,6 @@ pub fn update_settings(new_settings: Value) {
|
||||
}
|
||||
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
||||
db_lock.settings = new_settings;
|
||||
drop(db_lock);
|
||||
save_db();
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn fetch_settings() -> Settings {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
use std::{
|
||||
fs::{self, create_dir_all},
|
||||
mem::ManuallyDrop,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
@ -18,7 +20,6 @@ use super::models::data::Database;
|
||||
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
||||
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
|
||||
|
||||
|
||||
// Custom JSON serializer to support everything we need
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DropDatabaseSerializer;
|
||||
@ -27,15 +28,16 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||
for DropDatabaseSerializer
|
||||
{
|
||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||
native_model::rmp_serde_1_3::RmpSerde::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
||||
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
||||
.map_err(|e| DeSerError::Internal(e.to_string()))
|
||||
}
|
||||
|
||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||
let mut buf = Vec::new();
|
||||
s.read_to_end(&mut buf)
|
||||
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||
let val =
|
||||
native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
||||
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
@ -122,27 +124,34 @@ fn handle_invalid_database(
|
||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||
}
|
||||
|
||||
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {}", e);
|
||||
panic!("database borrow failed with error {}", e);
|
||||
}
|
||||
// To automatically save the database upon drop
|
||||
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
||||
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||
impl<'a> Deref for DBWrite<'a> {
|
||||
type Target = RwLockWriteGuard<'a, Database>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Deref for DBRead<'a> {
|
||||
type Target = RwLockReadGuard<'a, Database>;
|
||||
|
||||
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {}", e);
|
||||
panic!("database borrow mut failed with error {}", e);
|
||||
}
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'a> DerefMut for DBWrite<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Drop for DBWrite<'a> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.0);
|
||||
}
|
||||
|
||||
pub fn save_db() {
|
||||
match DB.save() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
@ -150,4 +159,24 @@ pub fn save_db() {
|
||||
panic!("database failed to save with error {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => DBRead(data),
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {}", e);
|
||||
panic!("database borrow failed with error {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => DBWrite(ManuallyDrop::new(data)),
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {}", e);
|
||||
panic!("database borrow mut failed with error {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
use crate::database::models::data::Database;
|
||||
|
||||
pub mod data {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use native_model::{native_model, Model};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type GameVersion = v1::GameVersion;
|
||||
pub type Database = v2::Database;
|
||||
pub type Database = v3::Database;
|
||||
pub type Settings = v1::Settings;
|
||||
pub type DatabaseAuth = v1::DatabaseAuth;
|
||||
|
||||
pub type GameDownloadStatus = v1::GameDownloadStatus;
|
||||
pub type GameDownloadStatus = v2::GameDownloadStatus;
|
||||
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
||||
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
||||
pub type DownloadType = v1::DownloadType;
|
||||
pub type DatabaseApplications = v1::DatabaseApplications;
|
||||
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||
|
||||
pub mod v1 {
|
||||
use crate::process::process_manager::Platform;
|
||||
use serde_with::serde_as;
|
||||
@ -102,7 +110,7 @@ pub mod data {
|
||||
}
|
||||
|
||||
// Stuff that shouldn't be synced to disk
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum ApplicationTransientStatus {
|
||||
Downloading { version_name: String },
|
||||
Uninstalling {},
|
||||
@ -162,14 +170,122 @@ pub mod data {
|
||||
}
|
||||
|
||||
pub mod v2 {
|
||||
use std::{collections::HashMap, path::PathBuf, process::Command};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct Database {
|
||||
#[serde(default)]
|
||||
pub settings: Settings,
|
||||
pub auth: Option<DatabaseAuth>,
|
||||
pub base_url: String,
|
||||
pub applications: v1::DatabaseApplications,
|
||||
#[serde(skip)]
|
||||
pub prev_database: Option<PathBuf>,
|
||||
pub cache_dir: PathBuf,
|
||||
pub compat_info: Option<DatabaseCompatInfo>,
|
||||
}
|
||||
|
||||
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
|
||||
pub struct DatabaseCompatInfo {
|
||||
pub umu_installed: bool,
|
||||
}
|
||||
|
||||
impl From<v1::Database> for Database {
|
||||
fn from(value: v1::Database) -> Self {
|
||||
Self {
|
||||
settings: value.settings,
|
||||
auth: value.auth,
|
||||
base_url: value.base_url,
|
||||
applications: value.applications,
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: crate::database::models::Database::create_new_compat_info(),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Strings are version names for a particular game
|
||||
#[derive(Serialize, Clone, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
pub enum GameDownloadStatus {
|
||||
Remote {},
|
||||
SetupRequired {
|
||||
version_name: String,
|
||||
install_dir: String,
|
||||
},
|
||||
Installed {
|
||||
version_name: String,
|
||||
install_dir: String,
|
||||
},
|
||||
PartiallyInstalled {
|
||||
version_name: String,
|
||||
install_dir: String,
|
||||
},
|
||||
}
|
||||
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
|
||||
fn from(value: v1::GameDownloadStatus) -> Self {
|
||||
match value {
|
||||
v1::GameDownloadStatus::Remote {} => Self::Remote {},
|
||||
v1::GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Self::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
},
|
||||
v1::GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Self::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Clone, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
pub struct DatabaseApplications {
|
||||
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>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
||||
}
|
||||
impl From<v1::DatabaseApplications> for DatabaseApplications {
|
||||
fn from(value: v1::DatabaseApplications) -> Self {
|
||||
Self {
|
||||
game_statuses: value
|
||||
.game_statuses
|
||||
.into_iter()
|
||||
.map(|x| (x.0, x.1.into()))
|
||||
.collect::<HashMap<String, GameDownloadStatus>>(),
|
||||
install_dirs: value.install_dirs,
|
||||
game_versions: value.game_versions,
|
||||
installed_game_version: value.installed_game_version,
|
||||
transient_statuses: value.transient_statuses,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mod v3 {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct Database {
|
||||
#[serde(default)]
|
||||
pub settings: Settings,
|
||||
@ -181,14 +297,20 @@ pub mod data {
|
||||
pub cache_dir: PathBuf,
|
||||
pub compat_info: Option<DatabaseCompatInfo>,
|
||||
}
|
||||
|
||||
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
|
||||
pub struct DatabaseCompatInfo {
|
||||
umu_installed: bool,
|
||||
impl From<v2::Database> for Database {
|
||||
fn from(value: v2::Database) -> Self {
|
||||
Self {
|
||||
settings: value.settings,
|
||||
auth: value.auth,
|
||||
base_url: value.base_url,
|
||||
applications: value.applications.into(),
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
@ -222,19 +344,4 @@ pub mod data {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v1::Database> for Database {
|
||||
fn from(value: v1::Database) -> Self {
|
||||
Self {
|
||||
settings: value.settings,
|
||||
auth: value.auth,
|
||||
base_url: value.base_url,
|
||||
applications: value.applications,
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
download_manager_builder::{CurrentProgressObject, DownloadAgent}, util::queue::Queue,
|
||||
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
||||
util::queue::Queue,
|
||||
};
|
||||
|
||||
pub enum DownloadManagerSignal {
|
||||
@ -71,6 +72,7 @@ impl Serialize for DownloadManagerStatus {
|
||||
pub enum DownloadStatus {
|
||||
Queued,
|
||||
Downloading,
|
||||
Validating,
|
||||
Error,
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,12 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||
downloadable::Downloadable, util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue},
|
||||
downloadable::Downloadable,
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::ProgressObject,
|
||||
queue::Queue,
|
||||
},
|
||||
};
|
||||
|
||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||
@ -243,13 +248,30 @@ impl DownloadManagerBuilder {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(true) => {
|
||||
debug!("download {:?} has completed", download_agent.metadata());
|
||||
match download_agent.validate() {
|
||||
Ok(true) => {
|
||||
download_agent.on_complete(&app_handle);
|
||||
sender
|
||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||
.unwrap();
|
||||
}
|
||||
Ok(false) => {
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"download {:?} has validation error {}",
|
||||
download_agent.metadata(),
|
||||
&e
|
||||
);
|
||||
download_agent.on_error(&app_handle, &e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ok(false) is for incomplete but exited properly
|
||||
Ok(false) => {
|
||||
debug!("Donwload agent finished incomplete");
|
||||
download_agent.on_incomplete(&app_handle);
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@ -8,13 +8,15 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||
download_manager::DownloadStatus,
|
||||
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||
};
|
||||
|
||||
pub trait Downloadable: Send + Sync {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
fn progress(&self) -> Arc<ProgressObject>;
|
||||
fn control_flag(&self) -> DownloadThreadControl;
|
||||
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
|
||||
fn status(&self) -> DownloadStatus;
|
||||
fn metadata(&self) -> DownloadableMetadata;
|
||||
fn on_initialised(&self, app_handle: &AppHandle);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
pub mod download_thread_control_flag;
|
||||
pub mod progress_object;
|
||||
pub mod queue;
|
||||
pub mod rolling_progress_updates;
|
||||
pub mod download_thread_control_flag;
|
||||
@ -12,9 +12,7 @@ use throttle_my_fn::throttle;
|
||||
|
||||
use crate::download_manager::download_manager::DownloadManagerSignal;
|
||||
|
||||
use super::{
|
||||
rolling_progress_updates::RollingProgressWindow,
|
||||
};
|
||||
use super::rolling_progress_updates::RollingProgressWindow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgressObject {
|
||||
@ -86,6 +84,17 @@ impl ProgressObject {
|
||||
.map(|instance| instance.load(Ordering::Relaxed))
|
||||
.sum()
|
||||
}
|
||||
pub fn reset(&self, size: usize) {
|
||||
self.set_time_now();
|
||||
self.set_size(size);
|
||||
self.bytes_last_update.store(0, Ordering::Release);
|
||||
self.rolling.reset();
|
||||
self.progress_instances
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.for_each(|x| x.store(0, Ordering::Release));
|
||||
}
|
||||
pub fn get_max(&self) -> usize {
|
||||
*self.max.lock().unwrap()
|
||||
}
|
||||
@ -124,7 +133,11 @@ pub fn calculate_update(progress: &ProgressObject) {
|
||||
|
||||
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
|
||||
|
||||
let bytes_remaining = max - current_bytes_downloaded; // bytes
|
||||
let bytes_remaining = if (max < current_bytes_downloaded) {
|
||||
0
|
||||
} else {
|
||||
max - current_bytes_downloaded
|
||||
}; // bytes
|
||||
|
||||
progress.update_window(kilobytes_per_second);
|
||||
push_update(progress, bytes_remaining);
|
||||
|
||||
@ -30,4 +30,9 @@ impl<const S: usize> RollingProgressWindow<S> {
|
||||
.sum::<usize>()
|
||||
/ S
|
||||
}
|
||||
pub fn reset(&self) {
|
||||
self.window
|
||||
.iter()
|
||||
.for_each(|x| x.store(0, Ordering::Release));
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ impl Display for ApplicationDownloadError {
|
||||
ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
|
||||
ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
|
||||
ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
|
||||
ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
|
||||
ApplicationDownloadError::IoError(error) => write!(f, "io error: {}", error),
|
||||
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ use serde_with::SerializeDisplay;
|
||||
pub enum BackupError {
|
||||
InvalidSystem,
|
||||
NotFound,
|
||||
ParseError
|
||||
ParseError,
|
||||
}
|
||||
|
||||
impl Display for BackupError {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
pub mod application_download_error;
|
||||
pub mod drop_server_error;
|
||||
pub mod backup_error;
|
||||
pub mod download_manager_error;
|
||||
pub mod drop_server_error;
|
||||
pub mod library_error;
|
||||
pub mod process_error;
|
||||
pub mod remote_access_error;
|
||||
pub mod setup_error;
|
||||
pub mod backup_error;
|
||||
@ -37,13 +37,14 @@ pub fn fetch_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
offline!(
|
||||
let res = offline!(
|
||||
state,
|
||||
fetch_game_logic,
|
||||
fetch_game_logic_offline,
|
||||
game_id,
|
||||
state
|
||||
)
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
download_manager::{
|
||||
download_manager::DownloadManagerSignal, downloadable::Downloadable,
|
||||
}, error::download_manager_error::DownloadManagerError, AppState
|
||||
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
|
||||
download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable},
|
||||
error::download_manager_error::DownloadManagerError,
|
||||
AppState,
|
||||
};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
@ -16,7 +20,7 @@ pub fn download_game(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
|
||||
game_id,
|
||||
game_version,
|
||||
install_dir,
|
||||
@ -28,3 +32,39 @@ pub fn download_game(
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_download(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||
let s = borrow_db_checked()
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game_id)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let (version_name, install_dir) = match s {
|
||||
GameDownloadStatus::Remote {} => unreachable!(),
|
||||
GameDownloadStatus::SetupRequired { .. } => unreachable!(),
|
||||
GameDownloadStatus::Installed { .. } => unreachable!(),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
};
|
||||
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>);
|
||||
Ok(state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)?)
|
||||
}
|
||||
|
||||
@ -1,23 +1,25 @@
|
||||
use crate::auth::generate_authorization_header;
|
||||
use crate::database::db::borrow_db_checked;
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use crate::database::models::data::{
|
||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata,
|
||||
};
|
||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||
use crate::download_manager::downloadable::Downloadable;
|
||||
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use crate::download_manager::util::download_thread_control_flag::{
|
||||
DownloadThreadControl, DownloadThreadControlFlag,
|
||||
};
|
||||
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::library::{on_game_complete, push_game_update, GameUpdateEvent};
|
||||
use crate::games::downloads::validate::game_validate_logic;
|
||||
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::DB;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use slice_deque::SliceDeque;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{create_dir_all, OpenOptions};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
@ -27,40 +29,48 @@ use tauri::{AppHandle, Emitter};
|
||||
use rustix::fs::{fallocate, FallocateFlags};
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
use super::stored_manifest::StoredManifest;
|
||||
use super::drop_data::DropData;
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||
completed_contexts: Mutex<SliceDeque<usize>>,
|
||||
context_map: Mutex<HashMap<String, bool>>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: StoredManifest,
|
||||
pub stored_manifest: DropData,
|
||||
status: Mutex<DownloadStatus>,
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub fn new(
|
||||
pub fn new_from_index(
|
||||
id: String,
|
||||
version: String,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let db_lock = borrow_db_checked();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
Self::new(id, version, base_dir, sender)
|
||||
}
|
||||
pub fn new(
|
||||
id: String,
|
||||
version: String,
|
||||
base_dir: PathBuf,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let base_dir_path = Path::new(&base_dir);
|
||||
let data_base_dir_path = base_dir_path.join(id.clone());
|
||||
|
||||
let stored_manifest =
|
||||
StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
||||
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
||||
|
||||
Self {
|
||||
id,
|
||||
@ -68,7 +78,7 @@ impl GameDownloadAgent {
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
completed_contexts: Mutex::new(SliceDeque::new()),
|
||||
context_map: Mutex::new(HashMap::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
@ -173,11 +183,15 @@ impl GameDownloadAgent {
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if !self.contexts.lock().unwrap().is_empty() {
|
||||
return Ok(());
|
||||
if self.contexts.lock().unwrap().is_empty() {
|
||||
self.generate_contexts()?;
|
||||
}
|
||||
|
||||
self.generate_contexts()?;
|
||||
self.context_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.extend(self.stored_manifest.get_contexts());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -189,20 +203,18 @@ impl GameDownloadAgent {
|
||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
{
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
completed_contexts_lock.clear();
|
||||
completed_contexts_lock
|
||||
.extend_from_slice(&self.stored_manifest.get_completed_contexts());
|
||||
}
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
let path = base_path.join(Path::new(&raw_path));
|
||||
|
||||
let container = path.parent().unwrap();
|
||||
create_dir_all(container).unwrap();
|
||||
|
||||
let file = File::create(path.clone()).unwrap();
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path.clone())
|
||||
.unwrap();
|
||||
let mut running_offset = 0;
|
||||
|
||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||
@ -225,6 +237,14 @@ impl GameDownloadAgent {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
}
|
||||
}
|
||||
let existing_contexts = self.stored_manifest.get_completed_contexts();
|
||||
self.stored_manifest.set_contexts(
|
||||
&contexts
|
||||
.iter()
|
||||
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
|
||||
.collect::<Vec<(String, bool)>>(),
|
||||
);
|
||||
|
||||
*self.contexts.lock().unwrap() = contexts;
|
||||
|
||||
Ok(())
|
||||
@ -243,13 +263,14 @@ impl GameDownloadAgent {
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let completed_indexes = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||
let completed_contexts = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_contexts.clone();
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
debug!("{:#?}", contexts);
|
||||
pool.scope(|scope| {
|
||||
let client = &reqwest::blocking::Client::new();
|
||||
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();
|
||||
@ -258,7 +279,7 @@ impl GameDownloadAgent {
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
|
||||
// If we've done this one already, skip it
|
||||
if self.completed_contexts.lock().unwrap().contains(&index) {
|
||||
if Some(&true) == context_map.get(&context.checksum) {
|
||||
progress_handle.skip(context.length);
|
||||
continue;
|
||||
}
|
||||
@ -274,7 +295,7 @@ impl GameDownloadAgent {
|
||||
("name", &context.file_name),
|
||||
("chunk", &context.index.to_string()),
|
||||
],
|
||||
|r| { r },
|
||||
|r| r,
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
@ -290,10 +311,18 @@ impl GameDownloadAgent {
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context, &self.control_flag, progress_handle, request)
|
||||
{
|
||||
Ok(res) => {
|
||||
if res {
|
||||
completed_indexes.push(index);
|
||||
Ok(true) => {
|
||||
debug!(
|
||||
"Finished context #{} with checksum {}",
|
||||
index, context.checksum
|
||||
);
|
||||
completed_indexes.push(context.checksum.clone());
|
||||
}
|
||||
Ok(false) => {
|
||||
debug!(
|
||||
"Didn't finish context #{} with checksum {}",
|
||||
index, context.checksum
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
@ -304,36 +333,46 @@ impl GameDownloadAgent {
|
||||
}
|
||||
});
|
||||
|
||||
let newly_completed = completed_indexes.to_owned();
|
||||
let newly_completed = completed_contexts.to_owned();
|
||||
|
||||
let completed_lock_len = {
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
let mut context_map_lock = self.context_map.lock().unwrap();
|
||||
for (_, item) in newly_completed.iter() {
|
||||
completed_contexts_lock.push_front(*item);
|
||||
context_map_lock.insert(item.clone(), true);
|
||||
}
|
||||
|
||||
completed_contexts_lock.len()
|
||||
context_map_lock.values().filter(|x| **x).count()
|
||||
};
|
||||
let context_map_lock = self.context_map.lock().unwrap();
|
||||
let contexts = contexts
|
||||
.iter()
|
||||
.map(|x| {
|
||||
(
|
||||
x.checksum.clone(),
|
||||
context_map_lock
|
||||
.get(&x.checksum)
|
||||
.cloned()
|
||||
.or(Some(false))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(String, bool)>>();
|
||||
drop(context_map_lock);
|
||||
|
||||
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||
if completed_lock_len != contexts.len() {
|
||||
self.stored_manifest.set_contexts(&contexts);
|
||||
self.stored_manifest.write();
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !contexts.iter().all(|x| x.1) {
|
||||
info!(
|
||||
"download agent for {} exited without completing ({}/{})",
|
||||
self.id.clone(),
|
||||
completed_lock_len,
|
||||
contexts.len(),
|
||||
);
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
|
||||
self.stored_manifest.write();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// We've completed
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::Completed(self.metadata()))
|
||||
.unwrap();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@ -372,7 +411,7 @@ impl Downloadable for GameDownloadAgent {
|
||||
|
||||
error!("error while managing download: {}", error);
|
||||
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
@ -390,16 +429,10 @@ impl Downloadable for GameDownloadAgent {
|
||||
|
||||
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
|
||||
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
||||
let meta = self.metadata();
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(GameDownloadStatus::Remote {}), None),
|
||||
version: None,
|
||||
},
|
||||
on_game_incomplete(
|
||||
&self.metadata(),
|
||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@ -409,4 +442,15 @@ impl Downloadable for GameDownloadAgent {
|
||||
fn status(&self) -> DownloadStatus {
|
||||
self.status.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Validating;
|
||||
game_validate_logic(
|
||||
&self.stored_manifest,
|
||||
self.contexts.lock().unwrap().clone(),
|
||||
self.progress.clone(),
|
||||
self.sender.clone(),
|
||||
&self.control_flag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,12 +6,13 @@ use crate::error::application_download_error::ApplicationDownloadError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::DropDownloadContext;
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use log::warn;
|
||||
use log::{debug, info, warn};
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::{RequestBuilder, Response};
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::{ErrorKind, Read};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{
|
||||
@ -93,19 +94,32 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||
let mut current_size = 0;
|
||||
loop {
|
||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
buf_writer.flush()?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let bytes_read = self.source.read(&mut copy_buf)?;
|
||||
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 {
|
||||
if current_size >= self.size {
|
||||
debug!(
|
||||
"finished with final size of {} vs {}",
|
||||
current_size, self.size
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf_writer.flush()?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@ -122,6 +136,10 @@ pub fn download_game_chunk(
|
||||
progress: ProgressHandle,
|
||||
request: RequestBuilder,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
debug!(
|
||||
"Starting download chunk {}, {}, {} #{}",
|
||||
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
|
||||
);
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
@ -130,10 +148,12 @@ pub fn download_game_chunk(
|
||||
let request = request.header("Authorization", generate_authorization_header());
|
||||
|
||||
let response = request
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
debug!("chunk request got status code: {}", response.status());
|
||||
let err = response.json().unwrap();
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(err),
|
||||
@ -156,13 +176,14 @@ pub fn download_game_chunk(
|
||||
));
|
||||
}
|
||||
|
||||
let mut pipeline = DropDownloadPipeline::new(
|
||||
response,
|
||||
destination,
|
||||
control_flag,
|
||||
progress,
|
||||
content_length.unwrap().try_into().unwrap(),
|
||||
);
|
||||
let length = content_length.unwrap().try_into().unwrap();
|
||||
|
||||
if length != ctx.length {
|
||||
return Err(ApplicationDownloadError::DownloadError);
|
||||
}
|
||||
|
||||
let mut pipeline =
|
||||
DropDownloadPipeline::new(response, destination, control_flag, progress, length);
|
||||
|
||||
let completed = pipeline
|
||||
.copy()
|
||||
@ -187,5 +208,10 @@ pub fn download_game_chunk(
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Successfully finished download #{}, copied {} bytes",
|
||||
ctx.checksum, length
|
||||
);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
105
src-tauri/src/games/downloads/drop_data.rs
Normal file
105
src-tauri/src/games/downloads/drop_data.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
use native_model::{Decode, Encode};
|
||||
|
||||
pub type DropData = v1::DropData;
|
||||
|
||||
static DROP_DATA_PATH: &str = ".dropdata";
|
||||
|
||||
pub mod v1 {
|
||||
use std::{path::PathBuf, sync::Mutex};
|
||||
|
||||
use native_model::native_model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
pub struct DropData {
|
||||
pub game_id: String,
|
||||
pub game_version: String,
|
||||
pub contexts: Mutex<Vec<(String, bool)>>,
|
||||
pub base_path: PathBuf,
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
Self {
|
||||
base_path,
|
||||
game_id,
|
||||
game_version,
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
debug!("Generating new dropdata for game {}", game_id);
|
||||
return DropData::new(game_id, game_version, base_path);
|
||||
}
|
||||
};
|
||||
|
||||
let mut s = Vec::new();
|
||||
match file.read_to_end(&mut s) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return DropData::new(game_id, game_version, base_path);
|
||||
}
|
||||
};
|
||||
|
||||
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
DropData::new(game_id, game_version, base_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match file.write_all(&manifest_raw) {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
|
||||
*self.contexts.lock().unwrap() = completed_contexts.to_owned();
|
||||
}
|
||||
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) -> Vec<(String, bool)> {
|
||||
info!(
|
||||
"Any contexts which are complete? {}",
|
||||
self.contexts.lock().unwrap().iter().any(|x| x.1)
|
||||
);
|
||||
self.contexts.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod commands;
|
||||
pub mod download_agent;
|
||||
mod download_logic;
|
||||
mod drop_data;
|
||||
mod manifest;
|
||||
mod stored_manifest;
|
||||
pub mod validate;
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_binary::binary_stream::Endian;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct StoredManifest {
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
pub completed_contexts: Mutex<Vec<usize>>,
|
||||
pub base_path: PathBuf,
|
||||
}
|
||||
|
||||
static DROP_DATA_PATH: &str = ".dropdata";
|
||||
|
||||
impl StoredManifest {
|
||||
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
Self {
|
||||
base_path,
|
||||
game_id,
|
||||
game_version,
|
||||
completed_contexts: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return StoredManifest::new(game_id, game_version, base_path),
|
||||
};
|
||||
|
||||
let mut s = Vec::new();
|
||||
match file.read_to_end(&mut s) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return StoredManifest::new(game_id, game_version, base_path);
|
||||
}
|
||||
};
|
||||
|
||||
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
StoredManifest::new(game_id, game_version, base_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
|
||||
Ok(json) => json,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match file.write_all(&manifest_raw) {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
|
||||
}
|
||||
pub fn get_completed_contexts(&self) -> Vec<usize> {
|
||||
self.completed_contexts.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
201
src-tauri/src/games/downloads/validate.rs
Normal file
201
src-tauri/src/games/downloads/validate.rs
Normal file
@ -0,0 +1,201 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
sync::{mpsc::Sender, Arc},
|
||||
};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use md5::Context;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
|
||||
use crate::{
|
||||
database::db::borrow_db_checked,
|
||||
download_manager::{
|
||||
download_manager::DownloadManagerSignal,
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::{ProgressHandle, ProgressObject},
|
||||
},
|
||||
},
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
|
||||
remote::{auth::generate_authorization_header, requests::make_request},
|
||||
};
|
||||
|
||||
pub fn game_validate_logic(
|
||||
dropdata: &DropData,
|
||||
contexts: Vec<DropDownloadContext>,
|
||||
progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
control_flag: &DownloadThreadControl,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
progress.reset(contexts.len());
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"validating game: {} with {} threads",
|
||||
dropdata.game_id, max_download_threads
|
||||
);
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
debug!("{:#?}", contexts);
|
||||
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
||||
pool.scope(|scope| {
|
||||
let client = &reqwest::blocking::Client::new();
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let client = client.clone();
|
||||
|
||||
let current_progress = progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
|
||||
let invalid_chunks_scoped = invalid_chunks.clone();
|
||||
let sender = 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.header("Authorization", generate_authorization_header()),
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
sender
|
||||
.send(DownloadManagerSignal::Error(
|
||||
ApplicationDownloadError::Communication(e),
|
||||
))
|
||||
.unwrap();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match validate_game_chunk(context, control_flag, progress_handle) {
|
||||
Ok(true) => {
|
||||
debug!(
|
||||
"Finished context #{} with checksum {}",
|
||||
index, context.checksum
|
||||
);
|
||||
}
|
||||
Ok(false) => {
|
||||
debug!(
|
||||
"Didn't finish context #{} with checksum {}",
|
||||
index, &context.checksum
|
||||
);
|
||||
invalid_chunks_scoped.push(context.checksum.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !invalid_chunks.is_empty() {
|
||||
info!(
|
||||
"validation of game id {} failed for chunks {:?}",
|
||||
dropdata.game_id.clone(),
|
||||
invalid_chunks
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn validate_game_chunk(
|
||||
ctx: &DropDownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
debug!(
|
||||
"Starting chunk validation {}, {}, {} #{}",
|
||||
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
|
||||
);
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut source = File::open(&ctx.path).unwrap();
|
||||
|
||||
if ctx.offset != 0 {
|
||||
source
|
||||
.seek(SeekFrom::Start(ctx.offset))
|
||||
.expect("Failed to seek to file offset");
|
||||
}
|
||||
|
||||
let mut hasher = md5::Context::new();
|
||||
|
||||
let completed =
|
||||
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap();
|
||||
if !completed {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let res = hex::encode(hasher.compute().0);
|
||||
if res != ctx.checksum {
|
||||
println!(
|
||||
"Checksum failed. Correct: {}, actual: {}",
|
||||
&ctx.checksum, &res
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Successfully finished verification #{}, copied {} bytes",
|
||||
ctx.checksum, ctx.length
|
||||
);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn validate_copy(
|
||||
source: &mut File,
|
||||
dest: &mut Context,
|
||||
size: usize,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
) -> 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, dest);
|
||||
let mut total_bytes = 0;
|
||||
|
||||
loop {
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
buf_writer.flush()?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut bytes_read = source.read(&mut copy_buf)?;
|
||||
total_bytes += bytes_read;
|
||||
|
||||
// If we read over (likely), truncate our read to
|
||||
// the right size
|
||||
if total_bytes > size {
|
||||
let over = total_bytes - size;
|
||||
bytes_read -= over;
|
||||
total_bytes = size;
|
||||
}
|
||||
|
||||
buf_writer.write_all(©_buf[0..bytes_read])?;
|
||||
progress.add(bytes_read);
|
||||
|
||||
if total_bytes >= size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf_writer.flush()?;
|
||||
Ok(true)
|
||||
}
|
||||
@ -4,10 +4,10 @@ use std::thread::spawn;
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Emitter;
|
||||
use tauri::AppHandle;
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use crate::database::models::data::{
|
||||
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||
};
|
||||
@ -18,9 +18,9 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||
use crate::remote::requests::make_request;
|
||||
use crate::{AppState, DB};
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FetchGameStruct {
|
||||
game: Game,
|
||||
status: GameStatusWithTransient,
|
||||
@ -144,7 +144,7 @@ pub fn fetch_game_logic(
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let mut state_handle = state.lock().unwrap();
|
||||
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
let handle = borrow_db_checked();
|
||||
|
||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
@ -220,7 +220,7 @@ pub fn fetch_game_logic_offline(
|
||||
id: String,
|
||||
_state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
let handle = borrow_db_checked();
|
||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
@ -313,6 +313,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} {
|
||||
db_handle
|
||||
@ -341,7 +345,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_handle.emit("update_library", {}).unwrap();
|
||||
@ -354,6 +357,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!("invalid previous state for uninstall, failing silently.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,6 +370,66 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn on_game_incomplete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
if meta.version.is_none() {
|
||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||
}
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
|f| f.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
let game_version: GameVersion = response.json()?;
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.entry(meta.id.clone())
|
||||
.or_default()
|
||||
.insert(meta.version.clone().unwrap(), game_version.clone());
|
||||
handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
let status = GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
};
|
||||
|
||||
handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(handle);
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(status), None),
|
||||
version: Some(game_version),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
@ -404,7 +469,6 @@ pub fn on_game_complete(
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
drop(handle);
|
||||
save_db();
|
||||
|
||||
let status = if game_version.setup_command.is_empty() {
|
||||
GameDownloadStatus::Installed {
|
||||
@ -424,7 +488,6 @@ pub fn on_game_complete(
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
save_db();
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
@ -468,7 +531,7 @@ pub fn update_game_configuration(
|
||||
game_id: String,
|
||||
options: FrontendGameOptions,
|
||||
) -> Result<(), LibraryError> {
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let installed_version = handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
@ -499,8 +562,5 @@ pub fn update_game_configuration(
|
||||
.unwrap()
|
||||
.insert(version.to_string(), existing_configuration);
|
||||
|
||||
drop(handle);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ mod error;
|
||||
mod process;
|
||||
mod remote;
|
||||
|
||||
use crate::database::db::DatabaseImpls;
|
||||
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
|
||||
use client::commands::fetch_state;
|
||||
use client::{
|
||||
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
||||
cleanup::{cleanup_and_exit, quit},
|
||||
};
|
||||
use client::commands::fetch_state;
|
||||
use database::commands::{
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
||||
fetch_system_data, update_settings,
|
||||
@ -156,6 +156,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
for (game_id, status) in statuses.into_iter() {
|
||||
match status {
|
||||
GameDownloadStatus::Remote {} => {}
|
||||
GameDownloadStatus::PartiallyInstalled { .. } => {}
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: _,
|
||||
install_dir,
|
||||
@ -259,6 +260,7 @@ pub fn run() {
|
||||
delete_game_in_collection,
|
||||
// Downloads
|
||||
download_game,
|
||||
resume_download,
|
||||
move_download_in_queue,
|
||||
pause_downloads,
|
||||
resume_downloads,
|
||||
|
||||
@ -193,6 +193,10 @@ impl ProcessManager<'_> {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
_ => return Err(ProcessError::NotDownloaded),
|
||||
};
|
||||
|
||||
@ -248,7 +252,11 @@ impl ProcessManager<'_> {
|
||||
version_name: _,
|
||||
install_dir: _,
|
||||
} => (&game_version.setup_command, &game_version.setup_args),
|
||||
GameDownloadStatus::Remote {} => unreachable!("nuh uh"),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => unreachable!("Game registered as 'Partially Installed'"),
|
||||
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
|
||||
};
|
||||
|
||||
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);
|
||||
@ -346,8 +354,6 @@ impl Platform {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const HOST: Platform = Self::Linux;
|
||||
|
||||
|
||||
|
||||
pub fn is_case_sensitive(&self) -> bool {
|
||||
match self {
|
||||
Self::Windows | Self::MacOs => false,
|
||||
@ -373,7 +379,7 @@ impl From<whoami::Platform> for Platform {
|
||||
whoami::Platform::Windows => Platform::Windows,
|
||||
whoami::Platform::Linux => Platform::Linux,
|
||||
whoami::Platform::MacOS => Platform::MacOs,
|
||||
_ => unimplemented!()
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||
db::{borrow_db_checked, borrow_db_mut_checked},
|
||||
models::data::DatabaseAuth,
|
||||
},
|
||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||
@ -122,8 +122,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
client_id: response_struct.id,
|
||||
web_token: None, // gets created later
|
||||
});
|
||||
drop(handle);
|
||||
save_db();
|
||||
}
|
||||
|
||||
let web_token = {
|
||||
@ -140,8 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let mut_auth = handle.auth.as_mut().unwrap();
|
||||
mut_auth.web_token = Some(web_token);
|
||||
drop(handle);
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
remote::{auth::generate_authorization_header, requests::make_request},
|
||||
AppState, AppStatus,
|
||||
@ -65,8 +65,6 @@ pub fn sign_out(app: AppHandle) {
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
save_db();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
|
||||
@ -5,9 +5,8 @@ use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{borrow_db_mut_checked, save_db},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
AppState, AppStatus,
|
||||
database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppState,
|
||||
AppStatus,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -40,9 +39,6 @@ pub fn use_remote_logic(
|
||||
|
||||
let mut db_state = borrow_db_mut_checked();
|
||||
db_state.base_url = base_url.to_string();
|
||||
drop(db_state);
|
||||
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use http::{
|
||||
uri::PathAndQuery,
|
||||
Request, Response, StatusCode, Uri,
|
||||
};
|
||||
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
||||
use reqwest::blocking::Client;
|
||||
use tauri::UriSchemeResponder;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "Drop Desktop Client",
|
||||
"version": "0.3.0-rc-4",
|
||||
"version": "0.3.0-rc-5",
|
||||
"identifier": "dev.drop.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev --port 1432",
|
||||
|
||||
2
types.ts
2
types.ts
@ -59,11 +59,13 @@ export enum GameStatusEnum {
|
||||
Uninstalling = "Uninstalling",
|
||||
SetupRequired = "SetupRequired",
|
||||
Running = "Running",
|
||||
PartiallyInstalled = "PartiallyInstalled"
|
||||
}
|
||||
|
||||
export type GameStatus = {
|
||||
type: GameStatusEnum;
|
||||
version_name?: string;
|
||||
install_dir?: string;
|
||||
};
|
||||
|
||||
export enum DownloadableType {
|
||||
|
||||
Reference in New Issue
Block a user