mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-16 01:31:22 +10:00
feat: Added basic url downloading
Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
@ -68,7 +68,7 @@
|
|||||||
Open Data Directory
|
Open Data Directory
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="() => openLogFile()"
|
@click="() => queue_url_download()"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
>
|
>
|
||||||
@ -115,6 +115,15 @@ dataDir.value = systemData.dataDir;
|
|||||||
const currentPlatform = await platform();
|
const currentPlatform = await platform();
|
||||||
platformInfo.value = currentPlatform;
|
platformInfo.value = currentPlatform;
|
||||||
|
|
||||||
|
async function queue_url_download() {
|
||||||
|
try {
|
||||||
|
await invoke("queue_url_download", { url: "https://codeload.github.com/Drop-OSS/drop-app/zip/refs/heads/develop"});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDataDir() {
|
async function openDataDir() {
|
||||||
if (!dataDir.value) return;
|
if (!dataDir.value) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
use crate::AppState;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
client::url_downloader::URLDownloader, download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable}, error::download_manager_error::DownloadManagerError, AppState
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_state(
|
pub fn fetch_state(
|
||||||
@ -9,3 +13,22 @@ pub fn fetch_state(
|
|||||||
drop(guard);
|
drop(guard);
|
||||||
Ok(cloned_state)
|
Ok(cloned_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn queue_url_download(
|
||||||
|
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||||
|
url: String
|
||||||
|
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||||
|
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||||
|
let game_download_agent = Arc::new(Box::new(URLDownloader::new(
|
||||||
|
String::from("Test URL Download"),
|
||||||
|
"/home/quexeky/Downloads/test_url_download",
|
||||||
|
sender,
|
||||||
|
url,
|
||||||
|
)) as Box<dyn Downloadable + Send + Sync>);
|
||||||
|
Ok(state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.download_manager
|
||||||
|
.queue_download(game_download_agent)?)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
pub mod autostart;
|
pub mod autostart;
|
||||||
pub mod cleanup;
|
pub mod cleanup;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod url_downloader;
|
||||||
178
src-tauri/src/client/url_downloader.rs
Normal file
178
src-tauri/src/client/url_downloader.rs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, create_dir_all},
|
||||||
|
io::{self, copy},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{mpsc::Sender, Arc, Mutex},
|
||||||
|
u64, usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{debug, error, warn};
|
||||||
|
use reqwest::redirect::Policy;
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{
|
||||||
|
db::borrow_db_checked,
|
||||||
|
models::data::{DownloadType, DownloadableMetadata},
|
||||||
|
},
|
||||||
|
download_manager::{
|
||||||
|
download_manager::{DownloadManagerSignal, DownloadStatus},
|
||||||
|
downloadable::Downloadable,
|
||||||
|
util::{
|
||||||
|
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||||
|
progress_object::{ProgressHandle, ProgressObject},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
games::downloads::download_logic::{DropDownloadPipeline, DropWriter},
|
||||||
|
DB,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct URLDownloader {
|
||||||
|
id: String,
|
||||||
|
version: String,
|
||||||
|
url: String,
|
||||||
|
control_flag: DownloadThreadControl,
|
||||||
|
progress: Arc<ProgressObject>,
|
||||||
|
target: PathBuf,
|
||||||
|
sender: Sender<DownloadManagerSignal>,
|
||||||
|
status: Mutex<DownloadStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct URLDownloaderManager {
|
||||||
|
current_offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl URLDownloader {
|
||||||
|
pub fn new<S: Into<String>, P: AsRef<Path>>(
|
||||||
|
id: String,
|
||||||
|
target: P,
|
||||||
|
sender: Sender<DownloadManagerSignal>,
|
||||||
|
url: S,
|
||||||
|
) -> Self {
|
||||||
|
// Don't run by default
|
||||||
|
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
version: String::new(),
|
||||||
|
control_flag,
|
||||||
|
target: target.as_ref().into(),
|
||||||
|
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||||
|
sender,
|
||||||
|
status: Mutex::new(DownloadStatus::Queued),
|
||||||
|
url: url.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
|
// TODO: Fix these unwraps and implement From<io::Error> for ApplicationDownloadError
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.redirect(Policy::default())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = client.head(&self.url).send().unwrap();
|
||||||
|
let content_length = response
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_LENGTH)
|
||||||
|
.map(|x| x.to_str().unwrap().parse().unwrap())
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
let response = client.get(&self.url).send().unwrap();
|
||||||
|
|
||||||
|
println!("{:?}, content length: {}", response, content_length);
|
||||||
|
|
||||||
|
self.set_progress_object_params(content_length);
|
||||||
|
|
||||||
|
let progress = self.progress.get(0);
|
||||||
|
|
||||||
|
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||||
|
|
||||||
|
let mut pipeline = DropDownloadPipeline::new(
|
||||||
|
response,
|
||||||
|
DropWriter::new(&self.target),
|
||||||
|
&self.control_flag,
|
||||||
|
progress_handle,
|
||||||
|
content_length,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
let completed = pipeline
|
||||||
|
.copy()
|
||||||
|
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||||
|
if !completed {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
fn set_progress_object_params(&self, max: usize) {
|
||||||
|
// Avoid re-setting it
|
||||||
|
if self.progress.get_max() != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progress.set_max(max);
|
||||||
|
self.progress.set_size(1);
|
||||||
|
self.progress.set_time_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Downloadable for URLDownloader {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let mut handle = DB.borrow_data_mut().unwrap();
|
||||||
|
handle
|
||||||
|
.applications
|
||||||
|
.transient_statuses
|
||||||
|
.remove(&self.metadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||||
|
println!("Completed url download");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
println!("Incomplete url download");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
|
||||||
|
println!("Cancelled url download");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> DownloadStatus {
|
||||||
|
self.status.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ use std::fs::{set_permissions, Permissions};
|
|||||||
use std::io::{ErrorKind, Read};
|
use std::io::{ErrorKind, Read};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::Path;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{File, OpenOptions},
|
fs::{File, OpenOptions},
|
||||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||||
@ -22,14 +23,14 @@ pub struct DropWriter<W: Write> {
|
|||||||
destination: W,
|
destination: W,
|
||||||
}
|
}
|
||||||
impl DropWriter<File> {
|
impl DropWriter<File> {
|
||||||
fn new(path: PathBuf) -> Self {
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||||
Self {
|
Self {
|
||||||
destination: OpenOptions::new().write(true).open(path).unwrap(),
|
destination: OpenOptions::new().create(true).write(true).open(path).unwrap(),
|
||||||
hasher: Context::new(),
|
hasher: Context::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut self) -> io::Result<Digest> {
|
pub fn finish(mut self) -> io::Result<Digest> {
|
||||||
self.flush().unwrap();
|
self.flush().unwrap();
|
||||||
Ok(self.hasher.compute())
|
Ok(self.hasher.compute())
|
||||||
}
|
}
|
||||||
@ -66,7 +67,7 @@ pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
|||||||
pub size: usize,
|
pub size: usize,
|
||||||
}
|
}
|
||||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||||
fn new(
|
pub fn new(
|
||||||
source: Response,
|
source: Response,
|
||||||
destination: DropWriter<File>,
|
destination: DropWriter<File>,
|
||||||
control_flag: &'a DownloadThreadControl,
|
control_flag: &'a DownloadThreadControl,
|
||||||
@ -82,7 +83,7 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy(&mut self) -> Result<bool, io::Error> {
|
pub fn copy(&mut self) -> Result<bool, io::Error> {
|
||||||
let copy_buf_size = 512;
|
let copy_buf_size = 512;
|
||||||
let mut copy_buf = vec![0; copy_buf_size];
|
let mut copy_buf = vec![0; copy_buf_size];
|
||||||
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||||
@ -102,6 +103,10 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
if current_size == self.size {
|
if current_size == self.size {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if bytes_read == 0 {
|
||||||
|
println!("Terminated stream");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod download_agent;
|
pub mod download_agent;
|
||||||
mod download_logic;
|
pub mod download_logic;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
mod stored_manifest;
|
mod stored_manifest;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ mod error;
|
|||||||
mod process;
|
mod process;
|
||||||
mod remote;
|
mod remote;
|
||||||
|
|
||||||
use crate::database::db::DatabaseImpls;
|
use crate::{client::commands::queue_url_download, database::db::DatabaseImpls};
|
||||||
use client::{
|
use client::{
|
||||||
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
||||||
cleanup::{cleanup_and_exit, quit},
|
cleanup::{cleanup_and_exit, quit},
|
||||||
@ -264,6 +264,7 @@ pub fn run() {
|
|||||||
resume_downloads,
|
resume_downloads,
|
||||||
cancel_game,
|
cancel_game,
|
||||||
uninstall_game,
|
uninstall_game,
|
||||||
|
queue_url_download,
|
||||||
// Processes
|
// Processes
|
||||||
launch_game,
|
launch_game,
|
||||||
kill_game,
|
kill_game,
|
||||||
|
|||||||
Reference in New Issue
Block a user