mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-16 01:31:22 +10:00
refactor(downloads): Moved all files relevant to game downloads to their own directory
Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
370
src-tauri/src/games/downloads/download_agent.rs
Normal file
370
src-tauri/src/games/downloads/download_agent.rs
Normal file
@ -0,0 +1,370 @@
|
||||
use crate::auth::generate_authorization_header;
|
||||
use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls};
|
||||
use crate::download_manager::application_download_error::ApplicationDownloadError;
|
||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use crate::download_manager::downloadable::Downloadable;
|
||||
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
|
||||
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
|
||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::games::library::{on_game_complete, push_game_update};
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use log::{debug, error, info};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{create_dir_all, remove_dir_all, File};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::spawn;
|
||||
use std::time::Instant;
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use rustix::fs::{fallocate, FallocateFlags};
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
use super::stored_manifest::StoredManifest;
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||
completed_contexts: Mutex<VecDeque<usize>>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: StoredManifest,
|
||||
status: Mutex<DownloadStatus>
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub fn new(
|
||||
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 = DB.borrow_data().unwrap();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
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());
|
||||
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
control_flag,
|
||||
manifest: Mutex::new(None),
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
completed_contexts: Mutex::new(VecDeque::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
info!("Ensured manifest exists");
|
||||
|
||||
self.ensure_contexts()?;
|
||||
info!("Ensured contexts exists");
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
info!("Setting up download");
|
||||
self.setup_download()?;
|
||||
info!("Setting progress object params");
|
||||
self.set_progress_object_params();
|
||||
info!("Running");
|
||||
let timer = Instant::now();
|
||||
push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() })));
|
||||
let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError);
|
||||
|
||||
info!(
|
||||
"{} took {}ms to download",
|
||||
self.id,
|
||||
timer.elapsed().as_millis()
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if self.manifest.lock().unwrap().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.download_manifest()
|
||||
}
|
||||
|
||||
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
let manifest_url = base_url
|
||||
.join(
|
||||
format!(
|
||||
"/api/v1/client/metadata/manifest?id={}&version={}",
|
||||
self.id,
|
||||
encode(&self.version)
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(manifest_url.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::ManifestDownloadFailed(
|
||||
response.status(),
|
||||
response.text().unwrap(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download = response.json::<DropManifest>().unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(ApplicationDownloadError::Lock)
|
||||
}
|
||||
|
||||
fn set_progress_object_params(&self) {
|
||||
// Avoid re-setting it
|
||||
if self.progress.get_max() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
|
||||
let length = contexts.len();
|
||||
|
||||
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
|
||||
|
||||
debug!("Setting ProgressObject max to {}", chunk_count);
|
||||
self.progress.set_max(chunk_count);
|
||||
debug!("Setting ProgressObject size to {}", length);
|
||||
self.progress.set_size(length);
|
||||
debug!("Setting ProgressObject time to now");
|
||||
self.progress.set_time_now();
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if !self.contexts.lock().unwrap().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.generate_contexts()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||
let game_id = self.id.clone();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let base_path = Path::new(&self.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(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 mut running_offset = 0;
|
||||
|
||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||
contexts.push(DropDownloadContext {
|
||||
file_name: raw_path.to_string(),
|
||||
version: chunk.version_name.to_string(),
|
||||
offset: running_offset,
|
||||
index,
|
||||
game_id: game_id.to_string(),
|
||||
path: path.clone(),
|
||||
checksum: chunk.checksums[index].clone(),
|
||||
length: *length,
|
||||
permissions: chunk.permissions,
|
||||
});
|
||||
running_offset += *length as u64;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
}
|
||||
}
|
||||
*self.contexts.lock().unwrap() = contexts;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<bool, ()> {
|
||||
info!("downloading game: {}", self.id);
|
||||
const DOWNLOAD_MAX_THREADS: usize = 1;
|
||||
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(DOWNLOAD_MAX_THREADS)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let completed_indexes = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||
|
||||
pool.scope(|scope| {
|
||||
for (index, context) in self.contexts.lock().unwrap().iter().enumerate() {
|
||||
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||
|
||||
let progress = self.progress.get(index); // Clone arcs
|
||||
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) {
|
||||
progress_handle.add(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let context = context.clone();
|
||||
let control_flag = self.control_flag.clone(); // Clone arcs
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context.clone(), control_flag, progress_handle) {
|
||||
Ok(res) => {
|
||||
if res {
|
||||
completed_indexes.push(index);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let newly_completed = completed_indexes.to_owned();
|
||||
|
||||
let completed_lock_len = {
|
||||
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||
for (item, _) in newly_completed.iter() {
|
||||
completed_contexts_lock.push_front(item);
|
||||
}
|
||||
|
||||
completed_contexts_lock.len()
|
||||
};
|
||||
|
||||
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||
if completed_lock_len != self.contexts.lock().unwrap().len() {
|
||||
info!("da for {} exited without completing", self.id.clone());
|
||||
self.stored_manifest
|
||||
.set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into());
|
||||
info!("Setting completed contexts");
|
||||
self.stored_manifest.write();
|
||||
info!("Wrote completed contexts");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// We've completed
|
||||
self.sender
|
||||
.send(DownloadManagerSignal::Completed(self.metadata()))
|
||||
.unwrap();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Downloadable for GameDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||
self.download(app_handle)
|
||||
}
|
||||
|
||||
fn progress(&self) -> Arc<ProgressObject> {
|
||||
self.progress.clone()
|
||||
}
|
||||
|
||||
fn control_flag(&self) -> DownloadThreadControl {
|
||||
self.control_flag.clone()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> DownloadableMetadata {
|
||||
DownloadableMetadata {
|
||||
id: self.id.clone(),
|
||||
version: Some(self.version.clone()),
|
||||
download_type: DownloadType::Game,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
return;
|
||||
}
|
||||
|
||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Error;
|
||||
app_handle
|
||||
.emit("download_error", error.to_string())
|
||||
.unwrap();
|
||||
|
||||
error!("error while managing download: {}", error);
|
||||
|
||||
set_game_status(app_handle, self.metadata(), |db_handle, meta| {
|
||||
db_handle.applications.transient_statuses.remove(meta);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).unwrap();
|
||||
}
|
||||
|
||||
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
return;
|
||||
}
|
||||
|
||||
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn status(&self) -> DownloadStatus {
|
||||
self.status.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
68
src-tauri/src/games/downloads/download_commands.rs
Normal file
68
src-tauri/src/games/downloads/download_commands.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, AppState};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_game(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
install_dir: usize,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(
|
||||
Box::new(GameDownloadAgent::new(game_id, game_version, install_dir, sender)) as Box<dyn Downloadable + Send + Sync>
|
||||
);
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)
|
||||
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pause_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.pause_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.resume_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn move_game_in_queue(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
old_index: usize,
|
||||
new_index: usize,
|
||||
) {
|
||||
state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.rearrange(old_index, new_index)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||
state.lock().unwrap().download_manager.cancel(meta)
|
||||
}
|
||||
|
||||
/*
|
||||
#[tauri::command]
|
||||
pub fn get_current_write_speed(state: tauri::State<'_, Mutex<AppState>>) {}
|
||||
*/
|
||||
|
||||
/*
|
||||
fn use_download_agent(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
game_id: String,
|
||||
) -> Result<Arc<GameDownloadAgent>, String> {
|
||||
let lock = state.lock().unwrap();
|
||||
let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?;
|
||||
Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure
|
||||
}
|
||||
*/
|
||||
212
src-tauri/src/games/downloads/download_logic.rs
Normal file
212
src-tauri/src/games/downloads/download_logic.rs
Normal file
@ -0,0 +1,212 @@
|
||||
use crate::auth::generate_authorization_header;
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::download_manager::application_download_error::ApplicationDownloadError;
|
||||
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||
use crate::download_manager::progress_object::ProgressHandle;
|
||||
use crate::games::downloads::manifest::DropDownloadContext;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use http::StatusCode;
|
||||
use log::{info, warn};
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::blocking::Response;
|
||||
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
use urlencoding::encode;
|
||||
|
||||
pub struct DropWriter<W: Write> {
|
||||
hasher: Context,
|
||||
destination: W,
|
||||
}
|
||||
impl DropWriter<File> {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
destination: OpenOptions::new().write(true).open(path).unwrap(),
|
||||
hasher: Context::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> io::Result<Digest> {
|
||||
self.flush().unwrap();
|
||||
Ok(self.hasher.compute())
|
||||
}
|
||||
}
|
||||
// Write automatically pushes to file and hasher
|
||||
impl Write for DropWriter<File> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
/*
|
||||
self.hasher.write_all(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Unable to write to hasher: {}", e),
|
||||
)
|
||||
})?;
|
||||
*/
|
||||
self.destination.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
// self.hasher.flush()?;
|
||||
self.destination.flush()
|
||||
}
|
||||
}
|
||||
// Seek moves around destination output
|
||||
impl Seek for DropWriter<File> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.destination.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DropDownloadPipeline<R: Read, W: Write> {
|
||||
pub source: R,
|
||||
pub destination: DropWriter<W>,
|
||||
pub control_flag: DownloadThreadControl,
|
||||
pub progress: ProgressHandle,
|
||||
pub size: usize,
|
||||
}
|
||||
impl DropDownloadPipeline<Response, File> {
|
||||
fn new(
|
||||
source: Response,
|
||||
destination: DropWriter<File>,
|
||||
control_flag: DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
size: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
destination,
|
||||
control_flag,
|
||||
progress,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||
let copy_buf_size = 512;
|
||||
let mut copy_buf = vec![0; copy_buf_size];
|
||||
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||
|
||||
let mut current_size = 0;
|
||||
loop {
|
||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let bytes_read = self.source.read(&mut copy_buf)?;
|
||||
current_size += bytes_read;
|
||||
|
||||
buf_writer.write_all(©_buf[0..bytes_read])?;
|
||||
self.progress.add(bytes_read);
|
||||
|
||||
if current_size == self.size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<Digest, io::Error> {
|
||||
let checksum = self.destination.finish()?;
|
||||
Ok(checksum)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download_game_chunk(
|
||||
ctx: DropDownloadContext,
|
||||
control_flag: DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
// If we're paused
|
||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let chunk_url = base_url
|
||||
.join(&format!(
|
||||
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
|
||||
// Encode the parts we don't trust
|
||||
ctx.game_id,
|
||||
encode(&ctx.version),
|
||||
encode(&ctx.file_name),
|
||||
ctx.index
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let response = client
|
||||
.get(chunk_url)
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
warn!("{}", response.text().unwrap());
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidCodeError(400),
|
||||
));
|
||||
}
|
||||
|
||||
let mut destination = DropWriter::new(ctx.path.clone());
|
||||
|
||||
if ctx.offset != 0 {
|
||||
destination
|
||||
.seek(SeekFrom::Start(ctx.offset))
|
||||
.expect("Failed to seek to file offset");
|
||||
}
|
||||
|
||||
let content_length = response.content_length();
|
||||
if content_length.is_none() {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse,
|
||||
));
|
||||
}
|
||||
|
||||
let mut pipeline = DropDownloadPipeline::new(
|
||||
response,
|
||||
destination,
|
||||
control_flag,
|
||||
progress,
|
||||
content_length.unwrap().try_into().unwrap(),
|
||||
);
|
||||
|
||||
let completed = pipeline
|
||||
.copy()
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
if !completed {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
// If we complete the file, set the permissions (if on Linux)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let permissions = Permissions::from_mode(ctx.permissions);
|
||||
set_permissions(ctx.path, permissions).unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
let checksum = pipeline
|
||||
.finish()
|
||||
.map_err(|e| GameDownloadError::IoError(e))?;
|
||||
|
||||
let res = hex::encode(checksum.0);
|
||||
if res != ctx.checksum {
|
||||
return Err(GameDownloadError::Checksum);
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
27
src-tauri/src/games/downloads/manifest.rs
Normal file
27
src-tauri/src/games/downloads/manifest.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub type DropManifest = HashMap<String, DropChunk>;
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropChunk {
|
||||
pub permissions: u32,
|
||||
pub ids: Vec<String>,
|
||||
pub checksums: Vec<String>,
|
||||
pub lengths: Vec<usize>,
|
||||
pub version_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DropDownloadContext {
|
||||
pub file_name: String,
|
||||
pub version: String,
|
||||
pub index: usize,
|
||||
pub offset: u64,
|
||||
pub game_id: String,
|
||||
pub path: PathBuf,
|
||||
pub checksum: String,
|
||||
pub length: usize,
|
||||
pub permissions: u32,
|
||||
}
|
||||
5
src-tauri/src/games/downloads/mod.rs
Normal file
5
src-tauri/src/games/downloads/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod download_agent;
|
||||
pub mod download_commands;
|
||||
mod download_logic;
|
||||
mod manifest;
|
||||
mod stored_manifest;
|
||||
81
src-tauri/src/games/downloads/stored_manifest.rs
Normal file
81
src-tauri/src/games/downloads/stored_manifest.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use log::error;
|
||||
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) => {
|
||||
error!("{}", 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: &Vec<usize>) {
|
||||
*self.completed_contexts.lock().unwrap() = completed_contexts.clone();
|
||||
}
|
||||
pub fn get_completed_contexts(&self) -> Vec<usize> {
|
||||
self.completed_contexts.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user