Depot API & executor launch (#173)

* feat: depot api downloads

* feat: frontend fixes and experimental webview store

* feat: sync downloader

* feat: cleanup and fixes

* feat: encrypted database and fixed resuming

* feat: launch option selector

* fix: autostart when no options

* fix: clippy

* fix: clippy x2

* feat: executor launch

* feat: executor launch

* feat: not installed error handling

* feat: better offline handling

* feat: dependency popup

* fix: cancelation and resuming issues

* feat: dedup by platform

* feat: new ui for additional components and fix dl manager clog

* feat: auto-queue dependencies

* feat: depot scanning and ranking

* feat: new library fetching stack

* In-app store page (Windows + macOS) (#176)

* feat: async store loading

* feat: fix overscroll behaviour

* fix: query params in server protocol

* fix: clippy
This commit is contained in:
DecDuck
2026-01-20 00:40:48 +00:00
committed by GitHub
parent 55fdaf51e1
commit fc69ae30ab
72 changed files with 3430 additions and 2732 deletions
+20 -3
View File
@@ -21,7 +21,8 @@ pub struct DropServerError {
#[derive(Debug, SerializeDisplay)]
pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>),
FetchErrorLegacy(Arc<reqwest::Error>),
FetchError(Arc<reqwest_middleware::Error>),
FetchErrorWS(Arc<reqwest_websocket::Error>),
ParsingError(ParseError),
InvalidEndpoint,
@@ -33,6 +34,7 @@ pub enum RemoteAccessError {
OutOfSync,
Cache(std::io::Error),
CorruptedState,
NoDepots,
}
impl Display for RemoteAccessError {
@@ -56,6 +58,15 @@ impl Display for RemoteAccessError {
.unwrap_or("Unknown error".to_string())
)
}
RemoteAccessError::FetchErrorLegacy(error) => write!(
f,
"{}: {}",
error,
error
.source()
.map(|v| v.to_string())
.unwrap_or("Unknown error".to_string())
),
RemoteAccessError::FetchErrorWS(error) => write!(
f,
"{}: {}",
@@ -93,13 +104,19 @@ impl Display for RemoteAccessError {
f,
"Drop encountered a corrupted internal state. Please report this to the developers, with details of reproduction."
),
}
RemoteAccessError::NoDepots => write!(f, "There are no download depots configured on the server. Contact your server admin."),
}
}
}
impl From<reqwest::Error> for RemoteAccessError {
fn from(err: reqwest::Error) -> Self {
RemoteAccessError::FetchError(Arc::new(err))
RemoteAccessError::FetchErrorLegacy(Arc::new(err))
}
}
impl From<reqwest_middleware::Error> for RemoteAccessError {
fn from(value: reqwest_middleware::Error) -> Self {
RemoteAccessError::FetchError(Arc::new(value))
}
}
impl From<reqwest_websocket::Error> for RemoteAccessError {
+1 -1
View File
@@ -1,4 +1,4 @@
use database::{DB, interface::DatabaseImpls};
use database::{DB};
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::{debug, warn};
use tauri::UriSchemeResponder;
+4
View File
@@ -1,3 +1,7 @@
#![feature(slice_concat_trait)]
#![feature(sync_nonpoison)]
#![feature(nonpoison_mutex)]
pub mod auth;
#[macro_use]
pub mod cache;
+8 -9
View File
@@ -1,18 +1,17 @@
use database::{DB, interface::DatabaseImpls};
use database::{DB};
use reqwest_middleware::Error;
use url::Url;
use crate::{
auth::generate_authorization_header, error::RemoteAccessError, utils::DROP_CLIENT_ASYNC,
};
pub fn generate_url<T: AsRef<str>>(
path_components: &[T],
query: &[(T, T)],
pub fn generate_url(
path_components: &[&str],
query: &[(&str, &str)],
) -> Result<Url, RemoteAccessError> {
let mut base_url = DB.fetch_base_url();
for endpoint in path_components {
base_url = base_url.join(endpoint.as_ref())?;
}
let path_appended = path_components.join("/");
let mut base_url = DB.fetch_base_url().join(&path_appended)?;
{
let mut queries = base_url.query_pairs_mut();
for (param, val) in query {
@@ -22,7 +21,7 @@ pub fn generate_url<T: AsRef<str>>(
Ok(base_url)
}
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, reqwest::Error> {
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, Error> {
DROP_CLIENT_ASYNC
.get(url)
.header("Authorization", generate_authorization_header())
+53 -41
View File
@@ -1,12 +1,11 @@
use std::str::FromStr;
use database::borrow_db_checked;
use http::{Request, Response, StatusCode, Uri, uri::PathAndQuery};
use http::{
HeaderMap, HeaderValue, Request, Response, StatusCode, Uri, header::USER_AGENT,
};
use log::{error, warn};
use tauri::UriSchemeResponder;
use utils::webbrowser_open::webbrowser_open;
use crate::utils::DROP_CLIENT_SYNC;
use crate::utils::DROP_CLIENT_ASYNC;
pub async fn handle_server_proto_offline_wrapper(
request: Request<Vec<u8>>,
@@ -36,6 +35,7 @@ pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: U
Response::builder()
.status(e)
.body(Vec::new())
.inspect_err(|v| warn!("{:?}", v))
.expect("Failed to build error response"),
);
}
@@ -43,48 +43,49 @@ pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: U
}
async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode> {
let db_handle = borrow_db_checked();
let auth = match db_handle.auth.as_ref() {
Some(auth) => auth,
None => {
error!("Could not find auth in database");
return Err(StatusCode::UNAUTHORIZED);
}
let (remote_uri, web_token) = {
let db_handle = borrow_db_checked();
let auth = match db_handle.auth.as_ref() {
Some(auth) => auth,
None => {
error!("Could not find auth in database");
return Err(StatusCode::UNAUTHORIZED);
}
};
let web_token = match &auth.web_token {
Some(token) => token.clone(),
None => return Err(StatusCode::UNAUTHORIZED),
};
let remote_uri = db_handle
.base_url
.parse::<Uri>()
.inspect_err(|v| warn!("{:?}", v))
.expect("Failed to parse base url");
(remote_uri, web_token)
};
let web_token = match &auth.web_token {
Some(token) => token,
None => return Err(StatusCode::UNAUTHORIZED),
};
let remote_uri = db_handle
.base_url
.parse::<Uri>()
.expect("Failed to parse base url");
let path = request.uri().path();
let mut new_uri = request.uri().clone().into_parts();
new_uri.path_and_query = Some(
PathAndQuery::from_str(&format!("{path}?noWrapper=true"))
.expect("Failed to parse request path in proto"),
);
new_uri.authority = remote_uri.authority().cloned();
new_uri.scheme = remote_uri.scheme().cloned();
let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
let new_uri = Uri::from_parts(new_uri)
.inspect_err(|v| warn!("{:?}", v))
.expect(err_msg);
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
let mut headers = HeaderMap::new();
request.headers().clone_into(&mut headers);
headers.remove(USER_AGENT);
headers.append(USER_AGENT, HeaderValue::from_static("Drop Desktop Client"));
headers.append(
"Authorization",
HeaderValue::from_str(&format!("Bearer {web_token}")).unwrap(),
);
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
webbrowser_open(new_uri.to_string());
return Ok(Response::new(Vec::new()));
}
let client = DROP_CLIENT_SYNC.clone();
let response = match client
let response = match DROP_CLIENT_ASYNC
.request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {web_token}"))
.headers(request.headers().clone())
.headers(headers)
.send()
.await
{
Ok(response) => response,
Err(e) => {
@@ -94,15 +95,26 @@ async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u
};
let response_status = response.status();
let response_body = match response.bytes() {
let mut client_http_response = Response::builder()
.status(response_status)
.header("Access-Control-Allow-Origin", "*");
{
let client_response_headers = client_http_response.headers_mut().unwrap();
for (header, header_value) in response.headers() {
client_response_headers.insert(header, header_value.clone());
}
};
let response_body = match response.bytes().await {
Ok(bytes) => bytes,
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
};
let http_response = Response::builder()
.status(response_status)
let client_http_response = client_http_response
.body(response_body.to_vec())
.inspect_err(|v| warn!("{:?}", v))
.expect("Failed to build server proto response");
Ok(http_response)
Ok(client_http_response)
}
+76 -4
View File
@@ -2,12 +2,20 @@ use std::{
fs::{self, File},
io::Read,
sync::LazyLock,
time::Duration,
};
use client::{app_state::AppState, app_status::AppStatus};
use database::db::DATA_ROOT_DIR;
use http::Extensions;
use log::{debug, info, warn};
use reqwest::Certificate;
use reqwest_middleware::{
ClientBuilder, ClientWithMiddleware, Error, Middleware, Next, Result,
reqwest::{Request, Response},
};
use serde::Deserialize;
use tauri::{AppHandle, Emitter, Manager, async_runtime::Mutex};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -21,9 +29,65 @@ impl DropHealthcheck {
}
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
pub static DROP_CLIENT_ASYNC: LazyLock<ClientWithMiddleware> = LazyLock::new(get_client_async);
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
pub static DROP_APP_HANDLE: LazyLock<Mutex<Option<AppHandle>>> = LazyLock::new(|| Mutex::new(None));
struct AutoOfflineMiddleware;
#[async_trait::async_trait]
impl Middleware for AutoOfflineMiddleware {
async fn handle(
&self,
req: Request,
extensions: &mut Extensions,
next: Next<'_>,
) -> Result<Response> {
let res = next.run(req, extensions).await;
match res {
Ok(res) => {
tauri::async_runtime::spawn(async move {
let lock = DROP_APP_HANDLE.lock().await;
if let Some(app_handle) = &*lock {
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
let mut state_lock = state.lock();
if state_lock.status == AppStatus::Offline {
state_lock.status = AppStatus::SignedIn;
app_handle
.emit("update_state", &*state_lock)
.expect("failed to emit state update");
}
};
});
Ok(res)
}
Err(err) => match err {
Error::Middleware(error) => Err(Error::Middleware(error)),
Error::Reqwest(error) => {
if error.is_connect() {
// Spawn to defer this action - the state will most likely be locked
tauri::async_runtime::spawn(async move {
let lock = DROP_APP_HANDLE.lock().await;
if let Some(app_handle) = &*lock {
let state =
app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
let mut state_lock = state.lock();
state_lock.status = AppStatus::Offline;
app_handle
.emit("update_state", &*state_lock)
.expect("failed to emit state update");
};
});
};
Err(Error::Reqwest(error))
}
},
}
}
}
fn fetch_certificates() -> Vec<Certificate> {
let certificate_dir = DATA_ROOT_DIR.join("certificates");
@@ -91,19 +155,26 @@ pub fn get_client_sync() -> reqwest::blocking::Client {
}
client
.use_rustls_tls()
.user_agent("Drop Desktop Client")
.connect_timeout(Duration::from_millis(1500))
.build()
.expect("Failed to build synchronous client")
}
pub fn get_client_async() -> reqwest::Client {
pub fn get_client_async() -> ClientWithMiddleware {
let mut client = reqwest::ClientBuilder::new();
for cert in DROP_CERT_BUNDLE.iter() {
client = client.add_root_certificate(cert.clone());
}
client
let normal_client = client
.use_rustls_tls()
.user_agent("Drop Desktop Client")
.build()
.expect("Failed to build asynchronous client");
ClientBuilder::new(normal_client)
.with(AutoOfflineMiddleware)
.build()
.expect("Failed to build asynchronous client")
}
pub fn get_client_ws() -> reqwest::Client {
let mut client = reqwest::ClientBuilder::new();
@@ -113,6 +184,7 @@ pub fn get_client_ws() -> reqwest::Client {
}
client
.use_rustls_tls()
.user_agent("Drop Desktop Client")
.http1_only()
.build()
.expect("Failed to build websocket client")