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:
DecDuck
2026-02-06 00:30:27 +11:00
committed by GitHub
parent 5ad005161f
commit b71809c041
23 changed files with 808 additions and 284 deletions
+9 -5
View File
@@ -5,9 +5,12 @@ export type QueueState = {
queue: Array<{
meta: DownloadableMetadata;
status: string;
progress: number | null;
current: number;
max: number;
dl_progress: number | null;
dl_current: number;
dl_max: number;
disk_progress: number | null;
disk_current: number;
disk_max: number;
}>;
status: string;
};
@@ -33,7 +36,8 @@ listen("update_stats", (event) => {
stats.value = event.payload as StatsState;
});
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
export const useDownloadHistory = () =>
useState<Array<number>>("history", () => []);
export function formatKilobytes(bytes: number): string {
const units = ["K", "M", "G", "T", "P"];
@@ -47,4 +51,4 @@ export function formatKilobytes(bytes: number): string {
}
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
}
+47 -14
View File
@@ -6,12 +6,16 @@
<div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
>
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
<span class="font-bold text-zinc-100"
>{{ formatKilobytes(stats.speed) }}B/s</span
>
<span class="text-xs text-zinc-400"
>{{ formatTime(stats.time) }} left</span
>
</div>
<div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
<div
class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]"
>
<div
v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }"
@@ -20,7 +24,7 @@
</div>
</div>
<draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<template #item="{ element }: ListIterable">
<li
v-if="games[element.meta.id]"
:key="element.meta.id"
@@ -50,21 +54,48 @@
{{ element.status }}
</p>
<div
v-if="element.progress"
v-if="element.dl_progress"
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
>
<div
class="h-2 bg-blue-600"
:style="{ width: `${element.progress * 100}%` }"
:style="{ width: `${element.dl_progress * 100}%` }"
/>
</div>
<span
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}B</span>
><span class="text-zinc-300"
>{{ formatKilobytes(element.dl_current / 1000) }}B</span
>
/
<span class="">{{ formatKilobytes(element.max / 1000) }}B</span
<span class=""
>{{ formatKilobytes(element.dl_max / 1000) }}B</span
><CloudIcon class="size-5"
/></span>
<div
v-if="element.dl_max !== element.disk_max"
class="h-[1px] my-2 w-full bg-zinc-700"
/>
<div
v-if="
element.disk_progress && element.dl_max !== element.disk_max
"
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
>
<div
class="h-2 bg-blue-600"
:style="{ width: `${element.disk_progress * 100}%` }"
/>
</div>
<span
v-if="element.dl_max !== element.disk_max"
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300"
>{{ formatKilobytes(element.disk_current / 1000) }}B</span
>
/
<span class=""
>{{ formatKilobytes(element.disk_max / 1000) }}B</span
><ServerIcon class="size-5"
/></span>
</div>
@@ -89,7 +120,7 @@
</template>
<script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { ServerIcon, XMarkIcon, CloudIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
@@ -108,9 +139,11 @@ const stats = useStatsState();
const speedHistory = useDownloadHistory();
const speedHistoryMax = computed(() => windowWidth.value / 4);
const speedMax = computed(
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1,
);
const previousGameId = useState<string | undefined>('previous_game');
const previousGameId = useState<string | undefined>("previous_game");
type ListIterable = { element: (typeof queue.value.queue)[0] };
const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
@@ -122,7 +155,7 @@ function resetHistoryGraph() {
}
function checkReset(v: QueueState) {
const currentGame = v.queue.at(0)?.meta.id;
// If we don't have a game
// If we don't have a game
if (!currentGame) return;
// If we're finished
@@ -150,7 +183,7 @@ watch(queue, (v) => {
});
watch(stats, (v) => {
if(v.speed == 0) return;
if (v.speed == 0) return;
const newLength = speedHistory.value.push(v.speed);
if (newLength > speedHistoryMax.value) {
speedHistory.value.splice(0, newLength - speedHistoryMax.value);
+345
View File
@@ -403,6 +403,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.10.1"
@@ -424,6 +430,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bincode"
version = "1.3.3"
@@ -852,6 +864,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
@@ -996,6 +1014,18 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array 0.14.7",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -1052,6 +1082,33 @@ dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "darling"
version = "0.21.3"
@@ -1141,6 +1198,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "der-parser"
version = "9.0.0"
@@ -1251,6 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"const-oid",
"crypto-common",
"subtle",
]
@@ -1548,12 +1617,71 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest 0.10.7",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2 0.10.9",
"subtle",
"zeroize",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest 0.10.7",
"ff",
"generic-array 0.14.7",
"group",
"hkdf",
"pem-rfc7468",
"pkcs8",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "embed-resource"
version = "3.0.6"
@@ -1694,6 +1822,22 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "field-offset"
version = "0.3.6"
@@ -2061,6 +2205,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
"zeroize",
]
[[package]]
@@ -2225,6 +2370,17 @@ dependencies = [
"system-deps",
]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "gtk"
version = "0.18.2"
@@ -2941,6 +3097,29 @@ dependencies = [
"serde_json",
]
[[package]]
name = "jsonwebtoken"
version = "10.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
"getrandom 0.2.17",
"hmac",
"js-sys",
"p256",
"p384",
"pem",
"rand 0.8.5",
"rsa",
"serde",
"serde_json",
"sha2 0.10.9",
"signature",
"simple_asn1",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -3004,6 +3183,9 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libappindicator"
@@ -3498,6 +3680,22 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -3551,6 +3749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -3985,6 +4184,30 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2 0.10.9",
]
[[package]]
name = "p384"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2 0.10.9",
]
[[package]]
name = "page_size"
version = "0.6.0"
@@ -4090,6 +4313,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -4273,6 +4505,27 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -4349,6 +4602,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -4790,6 +5052,7 @@ dependencies = [
"gethostname",
"hex 0.4.3",
"http 1.4.0",
"jsonwebtoken",
"log",
"md5 0.8.0",
"reqwest 0.12.28",
@@ -4957,6 +5220,16 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "rfd"
version = "0.16.0"
@@ -5028,6 +5301,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -5234,6 +5527,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array 0.14.7",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "secret-service"
version = "4.0.0"
@@ -5647,12 +5954,34 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core 0.6.4",
]
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.17",
"time",
]
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -5756,6 +6085,22 @@ dependencies = [
"system-deps",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "ssri"
version = "7.0.0"
+7 -4
View File
@@ -6,7 +6,7 @@ use std::{
use keyring::Entry;
use log::info;
use crate::interface::{DatabaseInterface};
use crate::interface::DatabaseInterface;
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
@@ -32,6 +32,9 @@ pub(crate) static KEY_IV: LazyLock<([u8; 16], [u8; 16])> = LazyLock::new(|| {
info!("created new database key");
buffer.to_vec()
});
let new = key.split_off(16);
(new.try_into().expect("failed to extract key"), key.try_into().expect("failed to extract iv"))
});
let iv: Vec<u8> = key.split_off(16);
(
key[0..16].try_into().expect("key wrong length"),
iv[0..16].try_into().expect("iv wrong length"),
)
});
+2 -1
View File
@@ -238,9 +238,10 @@ pub mod data {
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub cache_dir: PathBuf,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
}
+43 -22
View File
@@ -5,6 +5,7 @@ use std::{
};
use futures_util::StreamExt;
use log::warn;
use remote::{
error::RemoteAccessError,
requests::{generate_url, make_authenticated_get},
@@ -33,6 +34,7 @@ struct Depot {
manifest: Option<DepotManifest>,
latest_speed: Option<usize>, // bytes per second
current_downloads: SyncSemaphore,
enabled: bool
}
pub struct DepotManager {
@@ -53,6 +55,38 @@ impl DepotManager {
}
}
async fn sync_depot(&self, depot: &mut Depot) -> Result<(), RemoteAccessError> {
let manifest_url = Url::parse(&depot.endpoint)?.join("manifest.json")?;
let manifest = DROP_CLIENT_ASYNC.get(manifest_url).send().await?;
let manifest: DepotManifest = manifest.json().await?;
depot.manifest.replace(manifest);
let speedtest_url = Url::parse(&depot.endpoint)?.join("speedtest")?;
let speedtest = DROP_CLIENT_ASYNC.get(speedtest_url).send().await?;
let mut stream = speedtest.bytes_stream();
let start = Instant::now();
let mut total_length = 0;
while let Some(chunk) = stream.next().await {
let length = chunk?.len();
total_length += length;
if SPEEDTEST_TIMEOUT <= start.elapsed() {
break;
}
}
let elapsed = start.elapsed().as_millis() as usize;
let speed = if elapsed == 0 {
usize::MAX
} else {
(total_length / elapsed) * 1000
};
depot.latest_speed.replace(speed);
Ok(())
}
pub async fn sync_depots(&self) -> Result<(), RemoteAccessError> {
let depots = make_authenticated_get(generate_url(&["/api/v1/client/depots"], &[])?).await?;
let depots: Vec<ServersideDepot> = depots.json().await?;
@@ -68,33 +102,20 @@ impl DepotManager {
manifest: None,
latest_speed: None,
current_downloads: SyncSemaphore::new(),
enabled: true,
})
.collect::<Vec<Depot>>();
for depot in &mut new_depots {
let manifest_url = Url::parse(&depot.endpoint)?.join("manifest.json")?;
let manifest = DROP_CLIENT_ASYNC.get(manifest_url).send().await?;
let manifest: DepotManifest = manifest.json().await?;
depot.manifest.replace(manifest);
let speedtest_url = Url::parse(&depot.endpoint)?.join("speedtest")?;
let speedtest = DROP_CLIENT_ASYNC.get(speedtest_url).send().await?;
let mut stream = speedtest.bytes_stream();
let start = Instant::now();
let mut total_length = 0;
while let Some(chunk) = stream.next().await {
let length = chunk?.len();
total_length += length;
if SPEEDTEST_TIMEOUT <= start.elapsed() {
break;
}
if let Err(sync_error) = self.sync_depot(depot).await {
warn!("failed to sync depot {}: {:?}", depot.endpoint, sync_error);
depot.enabled = false;
}
let elapsed = start.elapsed().as_millis() as usize;
let speed = if elapsed == 0 { usize::MAX } else { (total_length / elapsed) * 1000 };
depot.latest_speed.replace(speed);
}
let enabled = new_depots.iter().filter(|v| v.enabled).count();
if enabled == 0 {
return Err(RemoteAccessError::NoDepots);
}
let mut depot_lock = self.depots.write().unwrap();
@@ -16,7 +16,9 @@ use crate::{
depot_manager::DepotManager,
download_manager_frontend::DownloadStatus,
error::ApplicationDownloadError,
frontend_updates::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
frontend_updates::{
DiskStatsUpdateEvent, DownloadStatsUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData,
},
};
use super::{
@@ -107,7 +109,13 @@ impl DownloadManagerBuilder {
info!("download manager exited with result: {:?}", result);
});
DownloadManager::new(terminator, queue, active_progress, command_sender, depot_manager)
DownloadManager::new(
terminator,
queue,
active_progress,
command_sender,
depot_manager,
)
}
fn set_status(&self, status: DownloadManagerStatus) {
@@ -187,8 +195,8 @@ impl DownloadManagerBuilder {
DownloadManagerSignal::UpdateUIQueue => {
self.push_ui_queue_update();
}
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
self.push_ui_stats_update(kbs, time);
DownloadManagerSignal::UpdateUIDownloadStats(kbs, time) => {
self.push_ui_download_stats_update(kbs, time);
}
DownloadManagerSignal::Finish => {
self.stop_and_wait_current_download().await;
@@ -266,17 +274,16 @@ impl DownloadManagerBuilder {
*download_thread_lock = Some(tauri::async_runtime::spawn(async move {
loop {
let download_result =
match download_agent.download(&app_handle).await {
// Ok(true) is for completed and exited properly
Ok(v) => v,
Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, &e);
send!(sender, DownloadManagerSignal::Error(e));
return;
}
};
let download_result = match download_agent.download(&app_handle).await {
// Ok(true) is for completed and exited properly
Ok(v) => v,
Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, &e);
send!(sender, DownloadManagerSignal::Error(e));
return;
}
};
// If the download gets canceled
// immediately return, on_cancelled gets called for us earlier
@@ -339,7 +346,7 @@ impl DownloadManagerBuilder {
send!(self.sender, DownloadManagerSignal::Go);
}
async fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
info!("got signal Error");
warn!("got signal Error");
if let Some(metadata) = self.download_queue.read().front()
&& let Some(current_agent) = self.download_agent_registry.get(metadata)
{
@@ -384,9 +391,8 @@ impl DownloadManagerBuilder {
self.push_ui_queue_update();
send!(self.sender, DownloadManagerSignal::Go);
}
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
let event_data = StatsUpdateEvent { speed: kbs, time };
fn push_ui_download_stats_update(&self, kbs: usize, time: usize) {
let event_data = DownloadStatsUpdateEvent { speed: kbs, time };
app_emit!(&self.app_handle, "update_stats", event_data);
}
fn push_ui_queue_update(&self) {
@@ -398,9 +404,12 @@ impl DownloadManagerBuilder {
QueueUpdateEventQueueData {
meta: DownloadableMetadata::clone(key),
status: val.status(),
progress: val.progress().get_progress(),
current: val.progress().sum(),
max: val.progress().get_max(),
dl_progress: val.dl_progress().get_progress(),
dl_current: val.dl_progress().sum(),
dl_max: val.dl_progress().get_max(),
disk_progress: val.disk_progress().get_progress(),
disk_current: val.disk_progress().sum(),
disk_max: val.disk_progress().get_max(),
}
})
.collect();
@@ -40,7 +40,7 @@ pub enum DownloadManagerSignal {
Error(ApplicationDownloadError),
/// Pushes UI update
UpdateUIQueue,
UpdateUIStats(usize, usize), //kb/s and seconds
UpdateUIDownloadStats(usize, usize), //kb/s and seconds
}
#[derive(Debug)]
@@ -106,7 +106,7 @@ impl DownloadManager {
download_queue,
progress,
command_sender,
depot_manager
depot_manager,
}
}
@@ -22,7 +22,8 @@ pub trait Downloadable: Send + Sync + Debug {
async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>;
fn dl_progress(&self) -> &Arc<ProgressObject>;
fn disk_progress(&self) -> &Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl;
fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata;
@@ -7,9 +7,12 @@ use crate::download_manager_frontend::DownloadStatus;
pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata,
pub status: DownloadStatus,
pub progress: f64,
pub current: usize,
pub max: usize,
pub dl_progress: f64,
pub dl_current: usize,
pub dl_max: usize,
pub disk_progress: f64,
pub disk_current: usize,
pub disk_max: usize,
}
#[derive(Serialize, Clone)]
@@ -18,7 +21,12 @@ pub struct QueueUpdateEvent {
}
#[derive(Serialize, Clone)]
pub struct StatsUpdateEvent {
pub struct DownloadStatsUpdateEvent {
pub speed: usize,
pub time: usize,
}
#[derive(Serialize, Clone)]
pub struct DiskStatsUpdateEvent {
pub speed: usize,
}
@@ -7,15 +7,22 @@ use std::{
};
use atomic_instant_full::AtomicInstant;
use utils::{lock, send};
use tokio::sync::mpsc::Sender;
use utils::{lock, send};
use crate::download_manager_frontend::DownloadManagerSignal;
use crate::{download_manager_frontend::DownloadManagerSignal, util::progress_object};
use super::rolling_progress_updates::RollingProgressWindow;
#[derive(Clone, Debug)]
pub enum ProgressType {
Download,
Disk,
}
#[derive(Clone, Debug)]
pub struct ProgressObject {
progress_type: ProgressType,
max: Arc<Mutex<usize>>,
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
start: Arc<Mutex<Instant>>,
@@ -45,7 +52,7 @@ impl ProgressHandle {
pub fn add(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::AcqRel);
tauri::async_runtime::spawn(calculate_update(self.progress_object.clone()));
spawn_update(&self.progress_object);
}
pub fn skip(&self, amount: usize) {
self.progress
@@ -59,7 +66,12 @@ impl ProgressHandle {
}
impl ProgressObject {
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
pub fn new(
max: usize,
length: usize,
sender: Sender<DownloadManagerSignal>,
progress_type: ProgressType,
) -> Self {
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
Self {
max: Arc::new(Mutex::new(max)),
@@ -70,6 +82,7 @@ impl ProgressObject {
last_update_time: Arc::new(AtomicInstant::now()),
bytes_last_update: Arc::new(AtomicUsize::new(0)),
rolling: RollingProgressWindow::new(),
progress_type,
}
}
@@ -111,17 +124,21 @@ impl ProgressObject {
}
}
pub async fn calculate_update(progress: Arc<ProgressObject>) {
let last_update_time = progress
.last_update_time
.load(Ordering::SeqCst);
pub fn spawn_update(progress: &Arc<ProgressObject>) {
let last_update_time = progress.last_update_time.load(Ordering::SeqCst);
let time_since_last_update = Instant::now()
.duration_since(last_update_time)
.as_millis_f64();
if time_since_last_update < 250.0 {
return;
}
progress.last_update_time.swap(Instant::now(), Ordering::SeqCst);
tauri::async_runtime::spawn(calculate_update(progress.clone(), time_since_last_update));
}
pub async fn calculate_update(progress: Arc<ProgressObject>, time_since_last_update: f64) {
progress
.last_update_time
.swap(Instant::now(), Ordering::SeqCst);
let current_bytes_downloaded = progress.sum();
let max = progress.get_max();
@@ -148,11 +165,18 @@ pub async fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
update_queue(progress).await;
}
async fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
send!(
progress_object.sender,
DownloadManagerSignal::UpdateUIStats(kilobytes_per_second, time_remaining)
);
async fn update_ui(
progress_object: &ProgressObject,
kilobytes_per_second: usize,
time_remaining: usize,
) {
match progress_object.progress_type {
ProgressType::Download => send!(
progress_object.sender,
DownloadManagerSignal::UpdateUIDownloadStats(kilobytes_per_second, time_remaining)
),
ProgressType::Disk => (),
}
}
async fn update_queue(progress: &ProgressObject) {
+159 -86
View File
@@ -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 {
+35 -19
View File
@@ -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)
-46
View File
@@ -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()
}
}
-1
View File
@@ -2,5 +2,4 @@ pub mod download_agent;
mod download_logic;
pub mod drop_data;
pub mod error;
mod manifest;
pub mod utils;
+23 -26
View File
@@ -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.");
+1 -1
View File
@@ -453,7 +453,7 @@ impl ProcessManager<'_> {
#[cfg(target_os = "windows")]
let mut command = Command::new("cmd");
#[cfg(target_os = "windows")]
command.raw_arg(format!("/C \"{}\"", &launch_string));
command.raw_arg(format!("/C \"{}\"", &launch_parameters.0));
info!(
"launching (in {}): {}",
+1
View File
@@ -14,6 +14,7 @@ droplet-rs = "0.7.3"
gethostname = "1.0.2"
hex = "0.4.3"
http = "1.3.1"
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
log = "0.4.28"
md5 = "0.8.0"
reqwest = { version = "0.12.28", default-features = false, features = [
+28 -5
View File
@@ -1,10 +1,15 @@
use std::{collections::HashMap, env};
use std::{
collections::HashMap,
env,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use chrono::Utc;
use client::{app_status::AppStatus, user::User};
use database::{DatabaseAuth, interface::borrow_db_checked};
use droplet_rs::ssl::sign_nonce;
use gethostname::gethostname;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use log::{error, warn};
use serde::{Deserialize, Serialize};
use url::Url;
@@ -60,18 +65,36 @@ impl From<HandshakeResponse> for DatabaseAuth {
}
}
#[derive(Serialize, Deserialize)]
struct Claims {
exp: usize,
nbf: usize,
}
pub fn generate_authorization_header() -> String {
let certs = {
let db = borrow_db_checked();
db.auth.clone().expect("Authorisation not initialised")
};
let nonce = Utc::now().timestamp_millis().to_string();
let system_time: usize = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs() as usize;
let signature =
sign_nonce(certs.private, nonce.clone()).expect("Failed to generate authorisation header");
let claims = Claims {
nbf: system_time,
exp: system_time + 10,
};
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
let jwt = jsonwebtoken::encode(
&Header::new(Algorithm::ES384),
&claims,
&EncodingKey::from_ec_pem(certs.private.as_bytes()).unwrap(),
)
.expect("failed to sign jwt");
format!("JWT {} {}", certs.client_id, jwt)
}
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
+10 -3
View File
@@ -1,6 +1,6 @@
use database::borrow_db_checked;
use http::{
HeaderMap, HeaderValue, Request, Response, StatusCode, Uri, header::USER_AGENT,
HeaderMap, HeaderValue, Request, Response, StatusCode, Uri, header::{CONTENT_SECURITY_POLICY, USER_AGENT, X_FRAME_OPTIONS},
};
use log::{error, warn};
use tauri::UriSchemeResponder;
@@ -30,7 +30,7 @@ pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: U
match handle_server_proto(request).await {
Ok(r) => responder.respond(r),
Err(e) => {
warn!("Cache error: {e}");
warn!("server proto error: {e}");
responder.respond(
Response::builder()
.status(e)
@@ -84,12 +84,13 @@ async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u
let response = match DROP_CLIENT_ASYNC
.request(request.method().clone(), new_uri.to_string())
.headers(headers)
.body(request.body().clone()) // TODO: refactor this into a move
.send()
.await
{
Ok(response) => response,
Err(e) => {
warn!("Could not send response. Got {e} when sending");
warn!("Could not send response. Got {e:?} when sending");
return Err(e.status().unwrap_or(StatusCode::BAD_REQUEST));
}
};
@@ -102,6 +103,12 @@ async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u
{
let client_response_headers = client_http_response.headers_mut().unwrap();
for (header, header_value) in response.headers() {
if header == CONTENT_SECURITY_POLICY {
continue;
}
if header == X_FRAME_OPTIONS {
continue;
}
client_response_headers.insert(header, header_value.clone());
}
};
+11 -6
View File
@@ -2,8 +2,8 @@ use std::sync::nonpoison::Mutex;
use bitcode::{Decode, Encode};
use database::{
DownloadableMetadata, GameDownloadStatus, borrow_db_checked,
borrow_db_mut_checked, platform::Platform,
DownloadableMetadata, GameDownloadStatus, borrow_db_checked, borrow_db_mut_checked,
platform::Platform,
};
use games::{
collections::collection::Collection,
@@ -190,7 +190,6 @@ pub async fn fetch_game_logic(
let db_lock = borrow_db_checked();
let metadata_option = db_lock.applications.installed_game_version.get(&id);
match metadata_option {
None => None,
@@ -258,7 +257,7 @@ struct VersionDownloadOptionRequiredContent {
name: String,
icon_object_id: String,
short_description: String,
size: usize,
size: GameSize,
}
#[derive(Serialize, Deserialize)]
@@ -268,9 +267,15 @@ pub struct VersionDownloadOption {
display_name: Option<String>,
version_path: String,
platform: Platform,
size: usize,
size: GameSize,
required_content: Vec<VersionDownloadOptionRequiredContent>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameSize {
install_size: usize,
download_size: usize,
}
pub async fn fetch_game_version_options_logic(
game_id: String,
@@ -278,7 +283,7 @@ pub async fn fetch_game_version_options_logic(
) -> Result<Vec<VersionDownloadOption>, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
let response = generate_url(&["/api/v1/client/game", &game_id, "versions"], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
+1 -1
View File
@@ -73,7 +73,7 @@ async fn setup(handle: AppHandle) -> AppState {
let console = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"{d} | {l} | {f}:{L} - {m}{n}",
"{d} | {h({l})} | {f}:{L} - {m}{n}",
)))
.build();