mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-07-24 00:42:59 +10:00
Game updates (#187)
* refactor: split umu launcher * feat: latest version picker + fixes * feat: frontend latest changes * feat: game update detection w/ setting * feat: fixes and refactor for game update * fix: windows ui * fix: deps * feat: update modifications * feat: missing ui and lock update * fix: create install dir on init * fix: clippy * fix: clippy x2 * feat: add configuration option to toggle updates * feat: uninstall dropdown on partiallyinstalled
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use database::models::data::UserConfiguration;
|
||||
use database::{
|
||||
ApplicationTransientStatus, DownloadableMetadata, borrow_db_checked, borrow_db_mut_checked,
|
||||
};
|
||||
@@ -22,6 +23,8 @@ use remote::utils::DROP_CLIENT_ASYNC;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::fs::{create_dir_all, remove_file};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
@@ -49,6 +52,7 @@ pub struct DownloadInformation {
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub metadata: DownloadableMetadata,
|
||||
pub configuration: UserConfiguration,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
pub dl_info: Mutex<Option<DownloadInformation>>,
|
||||
pub download_progress: Arc<ProgressObject>,
|
||||
@@ -66,41 +70,33 @@ impl Debug for GameDownloadAgent {
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub async fn new_from_index(
|
||||
metadata: DownloadableMetadata,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
depot_manager: Arc<DepotManager>,
|
||||
) -> Result<Self, ApplicationDownloadError> {
|
||||
let base_dir = {
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
db_lock.applications.install_dirs[target_download_dir].clone()
|
||||
};
|
||||
|
||||
Self::new(metadata, base_dir, sender, depot_manager).await
|
||||
}
|
||||
pub async fn new(
|
||||
metadata: DownloadableMetadata,
|
||||
base_dir: PathBuf,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
depot_manager: Arc<DepotManager>,
|
||||
configuration: UserConfiguration,
|
||||
) -> Result<Self, ApplicationDownloadError> {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let game_name = get_cached_object::<Game>(&format!("game/{}", metadata.id)).map(|v| v.library_path).unwrap_or(metadata.id.clone());
|
||||
let game_name = get_cached_object::<Game>(&format!("game/{}", metadata.id))
|
||||
.map(|v| v.library_path)
|
||||
.unwrap_or(metadata.id.clone());
|
||||
|
||||
let base_dir_path = Path::new(&base_dir);
|
||||
info!("base dir {}", base_dir_path.display());
|
||||
let data_base_dir_path = base_dir_path.join(game_name);
|
||||
info!("data dir path {}", data_base_dir_path.display());
|
||||
|
||||
create_dir_all(data_base_dir_path.clone())?;
|
||||
|
||||
let stored_manifest = DropData::generate(
|
||||
metadata.id.clone(),
|
||||
metadata.version.clone(),
|
||||
metadata.target_platform,
|
||||
data_base_dir_path.clone(),
|
||||
configuration.clone(),
|
||||
);
|
||||
|
||||
let result = Self {
|
||||
@@ -123,6 +119,7 @@ impl GameDownloadAgent {
|
||||
dropdata: stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
depot_manager,
|
||||
configuration,
|
||||
};
|
||||
|
||||
result.ensure_manifest_exists().await?;
|
||||
@@ -141,6 +138,21 @@ impl GameDownloadAgent {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn scan_filetree(&self, path: &Path) -> Result<Vec<PathBuf>, io::Error> {
|
||||
if !path.is_dir() {
|
||||
return Ok(vec![path.into()]);
|
||||
};
|
||||
|
||||
let subdirs = path.read_dir()?;
|
||||
let mut results = Vec::new();
|
||||
for subdir in subdirs {
|
||||
let subdir = subdir?;
|
||||
let subfiles = self.scan_filetree(&subdir.path())?;
|
||||
results.extend(subfiles);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
@@ -199,6 +211,13 @@ impl GameDownloadAgent {
|
||||
&[
|
||||
("id", &self.metadata.id),
|
||||
("version", &self.metadata.version),
|
||||
(
|
||||
"previous",
|
||||
self.dropdata
|
||||
.previously_installed_version
|
||||
.as_ref()
|
||||
.map_or("", |v| v),
|
||||
),
|
||||
],
|
||||
)
|
||||
.map_err(ApplicationDownloadError::Communication)?;
|
||||
@@ -277,13 +296,25 @@ impl GameDownloadAgent {
|
||||
let completed_chunks = lock!(self.dropdata.contexts);
|
||||
completed_chunks.clone()
|
||||
};
|
||||
info!("started with {} existing chunks", completed_chunks.len());
|
||||
let chunk_len = manifests_chunks.iter().map(|v| v.1.len()).sum::<usize>();
|
||||
let mut max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
if max_download_threads <= 0 {
|
||||
if max_download_threads == 0 {
|
||||
max_download_threads = 1;
|
||||
}
|
||||
|
||||
let file_list = &file_list;
|
||||
let base_path = &self.dropdata.base_path;
|
||||
let current_file_tree = self.scan_filetree(base_path)?;
|
||||
|
||||
for file in current_file_tree {
|
||||
let filename = file.strip_prefix(base_path)?.to_string_lossy().to_string();
|
||||
let needed = file_list.contains_key(&filename) || filename == ".dropdata";
|
||||
if !needed {
|
||||
debug!("deleted {}", file.display());
|
||||
remove_file(file)?;
|
||||
}
|
||||
}
|
||||
|
||||
let local_completed_chunks = completed_chunks.clone();
|
||||
|
||||
@@ -299,7 +330,7 @@ impl GameDownloadAgent {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
@@ -310,10 +341,8 @@ impl GameDownloadAgent {
|
||||
self.download_progress.get(index),
|
||||
self.download_progress.clone(),
|
||||
);
|
||||
let disk_progress_handle = ProgressHandle::new(
|
||||
self.disk_progress.get(index),
|
||||
self.disk_progress.clone(),
|
||||
);
|
||||
let disk_progress_handle =
|
||||
ProgressHandle::new(self.disk_progress.get(index), self.disk_progress.clone());
|
||||
index += 1;
|
||||
|
||||
let chunk_length = chunk_data.files.iter().map(|v| v.length).sum();
|
||||
@@ -344,7 +373,6 @@ impl GameDownloadAgent {
|
||||
}
|
||||
chunk_completions.push(async move {
|
||||
for i in 0..RETRY_COUNT {
|
||||
let base_path = self.dropdata.base_path.clone();
|
||||
match download_game_chunk(
|
||||
&self.metadata.id,
|
||||
&local_version_id,
|
||||
@@ -503,6 +531,7 @@ impl GameDownloadAgent {
|
||||
&self.metadata(),
|
||||
self.dropdata.base_path.display().to_string(),
|
||||
Some(app_handle),
|
||||
self.configuration.clone(),
|
||||
);
|
||||
|
||||
self.dropdata.write();
|
||||
@@ -573,6 +602,7 @@ impl Downloadable for GameDownloadAgent {
|
||||
async fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
match on_game_complete(
|
||||
&self.metadata(),
|
||||
self.configuration.clone(),
|
||||
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fs::{Permissions, set_permissions};
|
||||
use std::io::SeekFrom;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -37,7 +37,7 @@ pub async fn download_game_chunk(
|
||||
key: &[u8; 16],
|
||||
chunk_data: &ChunkData,
|
||||
file_list: &HashMap<String, String>,
|
||||
base_path: PathBuf,
|
||||
base_path: &Path,
|
||||
control_flag: &DownloadThreadControl,
|
||||
// How much we're downloading
|
||||
download_progress: &ProgressHandle,
|
||||
@@ -95,7 +95,7 @@ pub async fn download_game_chunk(
|
||||
|
||||
let stream = response
|
||||
.bytes_stream()
|
||||
.map(|v| v.map_err(|err| std::io::Error::other(err)));
|
||||
.map(|v| v.map_err(std::io::Error::other));
|
||||
let mut stream_reader = StreamReader::new(stream);
|
||||
//let mut stream_reader = response;
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use database::platform::Platform;
|
||||
use database::{models::data::UserConfiguration, platform::Platform};
|
||||
use log::error;
|
||||
use native_model::{Decode, Encode};
|
||||
use utils::lock;
|
||||
|
||||
pub type DropData = v1::DropData;
|
||||
@@ -17,38 +16,73 @@ pub static DROPDATA_PATH: &str = ".dropdata";
|
||||
pub mod v1 {
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||
|
||||
use database::platform::Platform;
|
||||
use native_model::native_model;
|
||||
use database::{models::data::UserConfiguration, platform::Platform};
|
||||
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 target_platform: Platform,
|
||||
#[serde(default)]
|
||||
pub configuration: UserConfiguration,
|
||||
pub contexts: Mutex<HashMap<String, bool>>,
|
||||
pub base_path: PathBuf,
|
||||
pub previously_installed_version: Option<String>,
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn new(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self {
|
||||
pub fn new(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
target_platform: Platform,
|
||||
base_path: PathBuf,
|
||||
configuration: UserConfiguration,
|
||||
previously_installed_version: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base_path,
|
||||
game_id,
|
||||
game_version,
|
||||
target_platform,
|
||||
contexts: Mutex::new(HashMap::new()),
|
||||
configuration,
|
||||
previously_installed_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DropData {
|
||||
pub fn generate(game_id: String, game_version: String, target_platform: Platform, base_path: PathBuf) -> Self {
|
||||
pub fn generate(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
target_platform: Platform,
|
||||
base_path: PathBuf,
|
||||
configuration: UserConfiguration,
|
||||
) -> Self {
|
||||
match DropData::read(&base_path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => DropData::new(game_id, game_version, target_platform, base_path),
|
||||
Ok(v) => {
|
||||
if v.game_id != game_id || v.game_version != game_version {
|
||||
return DropData::new(
|
||||
game_id,
|
||||
game_version,
|
||||
target_platform,
|
||||
base_path,
|
||||
configuration,
|
||||
Some(v.game_version),
|
||||
);
|
||||
}
|
||||
v
|
||||
}
|
||||
Err(_) => DropData::new(
|
||||
game_id,
|
||||
game_version,
|
||||
target_platform,
|
||||
base_path,
|
||||
configuration,
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
|
||||
@@ -57,7 +91,7 @@ impl DropData {
|
||||
let mut s = Vec::new();
|
||||
file.read_to_end(&mut s)?;
|
||||
|
||||
native_model::rmp_serde_1_3::RmpSerde::decode(s).map_err(|e| {
|
||||
pot::from_slice(&s).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Failed to decode drop data: {e}"),
|
||||
@@ -65,7 +99,7 @@ impl DropData {
|
||||
})
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||
let manifest_raw = match pot::to_vec(&self) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@ use bitcode::{Decode, Encode};
|
||||
use database::{
|
||||
ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||
borrow_db_checked, borrow_db_mut_checked,
|
||||
models::data::{InstalledGameType, UserConfiguration},
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use remote::{
|
||||
auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url,
|
||||
utils::DROP_CLIENT_ASYNC,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::remove_dir_all;
|
||||
use std::thread::spawn;
|
||||
@@ -76,8 +78,9 @@ pub fn set_partially_installed(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
configuration: UserConfiguration,
|
||||
) {
|
||||
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
|
||||
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle, configuration);
|
||||
}
|
||||
|
||||
pub fn set_partially_installed_db(
|
||||
@@ -85,13 +88,16 @@ pub fn set_partially_installed_db(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: Option<&AppHandle>,
|
||||
configuration: UserConfiguration,
|
||||
) {
|
||||
db_lock.applications.transient_statuses.remove(meta);
|
||||
db_lock.applications.game_statuses.insert(
|
||||
meta.id.clone(),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.clone(),
|
||||
GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::PartiallyInstalled { configuration },
|
||||
version_id: meta.version.clone(),
|
||||
install_dir,
|
||||
update_available: false,
|
||||
},
|
||||
);
|
||||
db_lock
|
||||
@@ -135,16 +141,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
|
||||
if let Some((_, install_dir)) = match previous_state {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => Some((version_name, install_dir)),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_type: _,
|
||||
version_id: version_name,
|
||||
install_dir,
|
||||
update_available: _,
|
||||
} => Some((version_name, install_dir)),
|
||||
_ => None,
|
||||
} {
|
||||
@@ -197,6 +197,7 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
|
||||
pub async fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
configuration: UserConfiguration,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
@@ -211,7 +212,12 @@ pub async fn on_game_complete(
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let game_version: GameVersion = response.json().await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
||||
}
|
||||
|
||||
let mut game_version: GameVersion = response.json().await?;
|
||||
game_version.user_configuration = configuration;
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
@@ -230,16 +236,15 @@ pub async fn on_game_complete(
|
||||
.iter()
|
||||
.find(|v| v.platform == meta.target_platform);
|
||||
|
||||
let status = if setup_configuration.is_none() {
|
||||
GameDownloadStatus::Installed {
|
||||
version_name: meta.version.clone(),
|
||||
install_dir,
|
||||
}
|
||||
} else {
|
||||
GameDownloadStatus::SetupRequired {
|
||||
version_name: meta.version.clone(),
|
||||
install_dir,
|
||||
}
|
||||
let status = GameDownloadStatus::Installed {
|
||||
version_id: meta.version.clone(),
|
||||
install_dir,
|
||||
install_type: if setup_configuration.is_none() {
|
||||
InstalledGameType::Installed
|
||||
} else {
|
||||
InstalledGameType::SetupRequired
|
||||
},
|
||||
update_available: false,
|
||||
};
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
@@ -247,10 +252,7 @@ pub async fn on_game_complete(
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.remove(meta);
|
||||
db_handle.applications.transient_statuses.remove(meta);
|
||||
drop(db_handle);
|
||||
app_emit!(
|
||||
app_handle,
|
||||
@@ -273,8 +275,10 @@ pub fn push_game_update(
|
||||
version: Option<GameVersion>,
|
||||
status: GameStatusWithTransient,
|
||||
) {
|
||||
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
||||
&status.0
|
||||
if let Some(GameDownloadStatus::Installed {
|
||||
install_type: InstalledGameType::Installed | InstalledGameType::SetupRequired,
|
||||
..
|
||||
}) = &status.0
|
||||
&& version.is_none()
|
||||
{
|
||||
panic!("pushed game for installed game that doesn't have version information");
|
||||
@@ -289,11 +293,4 @@ pub fn push_game_update(
|
||||
version,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrontendGameOptions {
|
||||
pub launch_string: String,
|
||||
pub override_proton_path: Option<String>,
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,22 @@ pub fn scan_install_dirs() {
|
||||
if !drop_data_file.exists() {
|
||||
continue;
|
||||
}
|
||||
let Ok(drop_data) = DropData::read(&game.path()) else {
|
||||
warn!(
|
||||
".dropdata exists for {}, but couldn't read it. is it corrupted?",
|
||||
game.file_name().display()
|
||||
);
|
||||
continue;
|
||||
let drop_data = match DropData::read(&game.path()) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
".dropdata exists for {}, but couldn't read it. is it corrupted? {:?}",
|
||||
game.file_name().display(),
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if db_lock.applications.game_statuses.contains_key(&drop_data.game_id) {
|
||||
if db_lock
|
||||
.applications
|
||||
.game_statuses
|
||||
.contains_key(&drop_data.game_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -41,6 +49,7 @@ pub fn scan_install_dirs() {
|
||||
&metadata,
|
||||
drop_data.base_path.to_str().unwrap().to_string(),
|
||||
None,
|
||||
drop_data.configuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user