In-app store, delta version support (#179)
* fix: windows launch * feat: add necessary client fixes for store * fix: keyring fix * feat: delta version support * feat: dl/disk progress * feat: move to jwt auth * fix: lint
This commit is contained in:
@@ -9,13 +9,15 @@ use download_manager::error::ApplicationDownloadError;
|
||||
use download_manager::util::download_thread_control_flag::{
|
||||
DownloadThreadControl, DownloadThreadControlFlag,
|
||||
};
|
||||
use download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||
use droplet_rs::manifest::Manifest;
|
||||
use download_manager::util::progress_object::{ProgressHandle, ProgressObject, ProgressType};
|
||||
use droplet_rs::manifest::{ChunkData, Manifest};
|
||||
use log::{debug, error, info, warn};
|
||||
use remote::auth::generate_authorization_header;
|
||||
use remote::error::RemoteAccessError;
|
||||
use remote::requests::generate_url;
|
||||
use remote::utils::DROP_CLIENT_ASYNC;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -34,11 +36,21 @@ use super::drop_data::DropData;
|
||||
|
||||
static RETRY_COUNT: usize = 3;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadInformation {
|
||||
file_list: HashMap<String, String>,
|
||||
manifests: HashMap<String, Manifest>,
|
||||
install_size: u64,
|
||||
download_size: u64,
|
||||
}
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub metadata: DownloadableMetadata,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
pub manifest: Mutex<Option<Manifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
pub dl_info: Mutex<Option<DownloadInformation>>,
|
||||
pub download_progress: Arc<ProgressObject>,
|
||||
pub disk_progress: Arc<ProgressObject>,
|
||||
depot_manager: Arc<DepotManager>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub dropdata: DropData,
|
||||
@@ -86,12 +98,23 @@ impl GameDownloadAgent {
|
||||
metadata.target_platform,
|
||||
data_base_dir_path.clone(),
|
||||
);
|
||||
|
||||
|
||||
let result = Self {
|
||||
metadata,
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
dl_info: Mutex::new(None),
|
||||
download_progress: Arc::new(ProgressObject::new(
|
||||
0,
|
||||
0,
|
||||
sender.clone(),
|
||||
ProgressType::Download,
|
||||
)),
|
||||
disk_progress: Arc::new(ProgressObject::new(
|
||||
0,
|
||||
0,
|
||||
sender.clone(),
|
||||
ProgressType::Disk,
|
||||
)),
|
||||
sender,
|
||||
dropdata: stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
@@ -100,7 +123,7 @@ impl GameDownloadAgent {
|
||||
|
||||
result.ensure_manifest_exists().await?;
|
||||
|
||||
let required_space = lock!(result.manifest).as_ref().unwrap().size;
|
||||
let required_space = lock!(result.dl_info).as_ref().unwrap().install_size;
|
||||
|
||||
let available_space = get_disk_available(data_base_dir_path)? as u64;
|
||||
|
||||
@@ -157,11 +180,11 @@ impl GameDownloadAgent {
|
||||
}
|
||||
|
||||
pub fn check_manifest_exists(&self) -> bool {
|
||||
lock!(self.manifest).is_some()
|
||||
lock!(self.dl_info).is_some()
|
||||
}
|
||||
|
||||
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if lock!(self.manifest).is_some() {
|
||||
if lock!(self.dl_info).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -195,12 +218,12 @@ impl GameDownloadAgent {
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download: Manifest = response
|
||||
let manifest_download: DownloadInformation = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
if let Ok(mut manifest) = self.dl_info.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -208,34 +231,62 @@ impl GameDownloadAgent {
|
||||
Err(ApplicationDownloadError::Lock)
|
||||
}
|
||||
|
||||
// Sets it up for both download and validate
|
||||
// Sets up progress for download writes
|
||||
fn setup_progress(&self) {
|
||||
let manifest = lock!(self.manifest);
|
||||
let manifest = manifest.as_ref().unwrap();
|
||||
let dl_info = lock!(self.dl_info);
|
||||
let dl_info = dl_info.as_ref().unwrap();
|
||||
|
||||
self.progress.set_max(manifest.size.try_into().unwrap());
|
||||
self.progress.set_size(manifest.chunks.len());
|
||||
self.progress.reset();
|
||||
let total_chunks = dl_info
|
||||
.manifests
|
||||
.iter()
|
||||
.map(|v| v.1.chunks.len())
|
||||
.sum::<usize>();
|
||||
|
||||
self.download_progress
|
||||
.set_max(dl_info.download_size.try_into().unwrap());
|
||||
self.download_progress
|
||||
.set_size(total_chunks);
|
||||
self.download_progress.reset();
|
||||
|
||||
self.disk_progress.set_max(dl_info.install_size.try_into().unwrap());
|
||||
self.disk_progress
|
||||
.set_size(total_chunks);
|
||||
self.disk_progress.reset();
|
||||
}
|
||||
|
||||
async fn run(&self) -> Result<bool, RemoteAccessError> {
|
||||
self.depot_manager.sync_depots().await?;
|
||||
info!("synced depots");
|
||||
self.setup_progress();
|
||||
let (chunks, key) = {
|
||||
let manifest = lock!(self.manifest);
|
||||
let manifest = manifest.as_ref().unwrap();
|
||||
(manifest.chunks.clone(), manifest.key)
|
||||
info!("setup progress objects");
|
||||
let manifests_chunks: Vec<(String, HashMap<String, ChunkData>, [u8; 16])> = {
|
||||
let dl_info = lock!(self.dl_info);
|
||||
dl_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.manifests
|
||||
.iter()
|
||||
.map(|v| (v.0.clone(), v.1.chunks.clone(), v.1.key))
|
||||
.collect()
|
||||
};
|
||||
let file_list = {
|
||||
let dl_info = lock!(self.dl_info);
|
||||
dl_info.as_ref().unwrap().file_list.clone()
|
||||
};
|
||||
let chunk_len = chunks.len();
|
||||
let mut completed_chunks = {
|
||||
let completed_chunks = lock!(self.dropdata.contexts);
|
||||
completed_chunks.clone()
|
||||
};
|
||||
let chunk_len = manifests_chunks.iter().map(|v| v.1.len()).sum::<usize>();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
let (sender, recv) = crossbeam_channel::bounded(16);
|
||||
|
||||
// SAFETY: I pinky-promise
|
||||
// (the scope keeps these in scope)
|
||||
let unsafe_self: &'static GameDownloadAgent = unsafe { mem::transmute(self) };
|
||||
let file_list: &'static HashMap<String, String> = unsafe { mem::transmute(&file_list) };
|
||||
|
||||
let local_completed_chunks = completed_chunks.clone();
|
||||
|
||||
let download_join_handle = tauri::async_runtime::spawn_blocking(move || {
|
||||
@@ -244,77 +295,95 @@ impl GameDownloadAgent {
|
||||
.build()
|
||||
.unwrap();
|
||||
thread_pool.scope(move |s| {
|
||||
for (index, (chunk_id, chunk_data)) in chunks.into_iter().enumerate() {
|
||||
let local_sender = sender.clone();
|
||||
let progress = unsafe_self.progress.get(index);
|
||||
let progress_handle =
|
||||
ProgressHandle::new(progress, unsafe_self.progress.clone());
|
||||
let mut index = 0;
|
||||
for (version_id, chunks, key) in manifests_chunks.into_iter() {
|
||||
let version_id = &version_id;
|
||||
for (chunk_id, chunk_data) in chunks.into_iter() {
|
||||
let local_sender = sender.clone();
|
||||
let download_progress_handle = ProgressHandle::new(
|
||||
unsafe_self.download_progress.get(index),
|
||||
unsafe_self.download_progress.clone(),
|
||||
);
|
||||
let disk_progress_handle = ProgressHandle::new(
|
||||
unsafe_self.disk_progress.get(index),
|
||||
unsafe_self.disk_progress.clone(),
|
||||
);
|
||||
index += 1;
|
||||
|
||||
let chunk_length = chunk_data.files.iter().map(|v| v.length).sum();
|
||||
let chunk_length = chunk_data.files.iter().map(|v| v.length).sum();
|
||||
|
||||
if *local_completed_chunks.get(&chunk_id).unwrap_or(&false) {
|
||||
progress_handle.skip(chunk_length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sender = unsafe_self.sender.clone();
|
||||
let (depot, permit) = match unsafe_self
|
||||
.depot_manager
|
||||
.next_depot(&unsafe_self.metadata.id, &unsafe_self.metadata.version)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send!(sender, DownloadManagerSignal::Error(ApplicationDownloadError::Communication(err)));
|
||||
});
|
||||
return;
|
||||
if *local_completed_chunks.get(&chunk_id).unwrap_or(&false) {
|
||||
download_progress_handle.skip(chunk_length);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
s.spawn(move |_| {
|
||||
for i in 0..RETRY_COUNT {
|
||||
let loop_progress_handle = progress_handle.clone();
|
||||
let base_path = unsafe_self.dropdata.base_path.clone();
|
||||
match download_game_chunk(
|
||||
&unsafe_self.metadata.id,
|
||||
&unsafe_self.metadata.version,
|
||||
&chunk_id,
|
||||
&depot,
|
||||
&key,
|
||||
&chunk_data,
|
||||
base_path,
|
||||
&unsafe_self.control_flag,
|
||||
loop_progress_handle,
|
||||
) {
|
||||
Ok(true) => {
|
||||
local_sender.send(chunk_id.clone()).unwrap();
|
||||
drop(permit); // Take ownership
|
||||
return;
|
||||
}
|
||||
Ok(false) => return,
|
||||
Err(e) => {
|
||||
warn!("got error for chunk id {}: {e:?}", chunk_id);
|
||||
let sender = unsafe_self.sender.clone();
|
||||
let (depot, permit) = match unsafe_self
|
||||
.depot_manager
|
||||
.next_depot(&unsafe_self.metadata.id, &unsafe_self.metadata.version)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send!(
|
||||
sender,
|
||||
DownloadManagerSignal::Error(
|
||||
ApplicationDownloadError::Communication(err)
|
||||
)
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let retry = true; /*matches!(
|
||||
&e,
|
||||
ApplicationDownloadError::Communication(_)
|
||||
| ApplicationDownloadError::Checksum
|
||||
| ApplicationDownloadError::Lock
|
||||
| ApplicationDownloadError::IoError(_)
|
||||
);*/
|
||||
|
||||
if i == RETRY_COUNT - 1 || !retry {
|
||||
warn!("retry logic failed, not re-attempting.");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send!(sender, DownloadManagerSignal::Error(e));
|
||||
});
|
||||
let local_version_id = version_id.clone();
|
||||
s.spawn(move |_| {
|
||||
for i in 0..RETRY_COUNT {
|
||||
let base_path = unsafe_self.dropdata.base_path.clone();
|
||||
match download_game_chunk(
|
||||
&unsafe_self.metadata.id,
|
||||
&local_version_id,
|
||||
&chunk_id,
|
||||
&depot,
|
||||
&key,
|
||||
&chunk_data,
|
||||
file_list,
|
||||
base_path,
|
||||
&unsafe_self.control_flag,
|
||||
&download_progress_handle,
|
||||
&disk_progress_handle,
|
||||
) {
|
||||
Ok(true) => {
|
||||
local_sender.send(chunk_id.clone()).unwrap();
|
||||
drop(permit); // Take ownership
|
||||
return;
|
||||
}
|
||||
Ok(false) => return,
|
||||
Err(e) => {
|
||||
warn!("got error for chunk id {}: {e:?}", chunk_id);
|
||||
|
||||
let retry = true; /*matches!(
|
||||
&e,
|
||||
ApplicationDownloadError::Communication(_)
|
||||
| ApplicationDownloadError::Checksum
|
||||
| ApplicationDownloadError::Lock
|
||||
| ApplicationDownloadError::IoError(_)
|
||||
);*/
|
||||
|
||||
if i == RETRY_COUNT - 1 || !retry {
|
||||
warn!("retry logic failed, not re-attempting.");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send!(sender, DownloadManagerSignal::Error(e));
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
drop(sender);
|
||||
});
|
||||
});
|
||||
@@ -457,8 +526,12 @@ impl Downloadable for GameDownloadAgent {
|
||||
self.validate(app_handle)
|
||||
}
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject> {
|
||||
self.progress.clone()
|
||||
fn dl_progress(&self) -> &Arc<ProgressObject> {
|
||||
&self.download_progress
|
||||
}
|
||||
|
||||
fn disk_progress(&self) -> &Arc<ProgressObject> {
|
||||
&self.disk_progress
|
||||
}
|
||||
|
||||
fn control_flag(&self) -> DownloadThreadControl {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
use std::io::{Read, Seek as _, SeekFrom, Write as _};
|
||||
#[cfg(unix)]
|
||||
@@ -32,13 +33,18 @@ pub fn download_game_chunk(
|
||||
depot: &str,
|
||||
key: &[u8; 16],
|
||||
chunk_data: &ChunkData,
|
||||
file_list: &HashMap<String, String>,
|
||||
base_path: PathBuf,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
// How much we're downloading
|
||||
download_progress: &ProgressHandle,
|
||||
// How much we're writing to disk
|
||||
disk_progress: &ProgressHandle,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
download_progress.set(0);
|
||||
disk_progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -48,10 +54,7 @@ pub fn download_game_chunk(
|
||||
|
||||
let url = Url::parse(depot)
|
||||
.map_err(|v| ApplicationDownloadError::DownloadError(v.into()))?
|
||||
.join(&format!(
|
||||
"content/{}/{}/{}",
|
||||
game_id, version_id, chunk_id
|
||||
))
|
||||
.join(&format!("content/{}/{}/{}", game_id, version_id, chunk_id))
|
||||
.map_err(|v| ApplicationDownloadError::DownloadError(v.into()))?;
|
||||
|
||||
let response = DROP_CLIENT_SYNC
|
||||
@@ -77,7 +80,8 @@ pub fn download_game_chunk(
|
||||
}
|
||||
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
download_progress.set(0);
|
||||
disk_progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -95,27 +99,39 @@ pub fn download_game_chunk(
|
||||
let mut cipher = Aes128Ctr64LE::new(key.into(), &chunk_data.iv.into());
|
||||
let mut read_buf = vec![0u8; READ_BUF_LEN];
|
||||
for file in &chunk_data.files {
|
||||
let should_write = file_list
|
||||
.get(&file.filename)
|
||||
.map(|v| v == version_id)
|
||||
.unwrap_or(false);
|
||||
let path = base_path.join(file.filename.clone());
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut file_handle = std::fs::OpenOptions::new()
|
||||
.truncate(false)
|
||||
.write(true)
|
||||
.append(false)
|
||||
.create(true)
|
||||
.open(&path)?;
|
||||
file_handle.seek(SeekFrom::Start(file.start.try_into().unwrap()))?;
|
||||
let mut file_handle = if should_write {
|
||||
let mut file_handle = std::fs::OpenOptions::new()
|
||||
.truncate(false)
|
||||
.write(true)
|
||||
.append(false)
|
||||
.create(true)
|
||||
.open(&path)?;
|
||||
file_handle.seek(SeekFrom::Start(file.start.try_into().unwrap()))?;
|
||||
Some(file_handle)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut remaining = file.length;
|
||||
while remaining > 0 {
|
||||
let amount = stream_reader.read(&mut read_buf[0..remaining.min(READ_BUF_LEN)])?;
|
||||
progress.add(amount);
|
||||
download_progress.add(amount);
|
||||
remaining -= amount;
|
||||
|
||||
cipher.apply_keystream(&mut read_buf[0..amount]);
|
||||
hasher.update(&read_buf[0..amount]);
|
||||
file_handle.write_all(&read_buf[0..amount])?;
|
||||
//hasher.update(&read_buf[0..amount]);
|
||||
if let Some(file_handle) = &mut file_handle {
|
||||
file_handle.write_all(&read_buf[0..amount])?;
|
||||
disk_progress.add(amount);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -132,14 +148,14 @@ pub fn download_game_chunk(
|
||||
}
|
||||
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
download_progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let digest = hex::encode(hasher.finalize());
|
||||
if digest != chunk_data.checksum {
|
||||
return Err(ApplicationDownloadError::Checksum);
|
||||
//return Err(ApplicationDownloadError::Checksum);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
// 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, Serialize)]
|
||||
pub struct DownloadBucket {
|
||||
pub game_id: String,
|
||||
pub version: String,
|
||||
pub drops: Vec<DownloadDrop>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DropValidateContext {
|
||||
pub index: usize,
|
||||
pub offset: usize,
|
||||
pub path: PathBuf,
|
||||
pub checksum: String,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,4 @@ pub mod download_agent;
|
||||
mod download_logic;
|
||||
pub mod drop_data;
|
||||
pub mod error;
|
||||
mod manifest;
|
||||
pub mod utils;
|
||||
@@ -5,10 +5,8 @@ use database::{
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use remote::{
|
||||
auth::generate_authorization_header,
|
||||
error::RemoteAccessError,
|
||||
requests::generate_url,
|
||||
utils::DROP_CLIENT_ASYNC
|
||||
auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url,
|
||||
utils::DROP_CLIENT_ASYNC,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::remove_dir_all;
|
||||
@@ -160,29 +158,28 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
spawn(move || {
|
||||
if let Err(e) = remove_dir_all(install_dir) {
|
||||
error!("{e}");
|
||||
} else {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
|
||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_emit!(&app_handle, "update_library", ());
|
||||
}
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
|
||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
app_emit!(&app_handle, "update_library", ());
|
||||
});
|
||||
} else {
|
||||
warn!("invalid previous state for uninstall, failing silently.");
|
||||
|
||||
Reference in New Issue
Block a user