Compare commits

...

15 Commits

Author SHA1 Message Date
682c6e9c0b Bump version to v0.3.0-rc-8 (#74) 2025-07-25 22:21:59 +10:00
46e1f16cdd Process manager fixes (#71)
* fix: launching on linux

* feat: #70

* feat: add dummy store page

* feat: add store redir and refresh button to library

* feat: cache first object fetching

* feat: Remove let_chains feature and update to Rust 2024

Signed-off-by: quexeky <git@quexeky.dev>

* feat: Check for if process was manually stopped

Signed-off-by: quexeky <git@quexeky.dev>

* fix: use bitcode instead of serde

* chore: remove logs

* fix: clippy

* fix: clippy 2

* fix: swap to stop icon

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-07-25 10:44:40 +10:00
d19f9bbc31 Fix client running behind reverse proxy (#69)
* fix: reverse proxy 400 due to duplicate header

* fix: clippy

* bump version and update ci
2025-07-18 20:08:12 +10:00
2913fdf35b Release v0.3.0-rc-6 (#68) 2025-07-18 17:38:36 +10:00
f9fdf151ea Clippy CI/CD (#67)
* feat: add clippy ci

* fix: clippy errors

* fix: ci/cd

* fix: update ci packages

* fix: add gtk3 to ci deps

* fix: add webkit to ci deps

* fix: ci deps and perms

* fix: add clippy settings to lib.rs
2025-07-18 17:36:04 +10:00
495d93705e Panic hook to generate crash dumps #65 (#66) 2025-07-18 16:35:02 +10:00
c477dd4872 Fix windows build by removing linux extension import (#64) 2025-07-14 16:43:11 +10:00
f560a62c8f Download fixes (#63)
* refactor: Rename StoredManifest to DropData

Signed-off-by: quexeky <git@quexeky.dev>

* fix: Downloads when resuming would truncate files which had not been finished

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Didn't import debug macro

Signed-off-by: quexeky <git@quexeky.dev>

* fix: Download chunks with wrong indexes

Migrated to using checksums as indexes instead

Signed-off-by: quexeky <git@quexeky.dev>

* feat: Resume download button

Also added DBWrite and DBRead structs to make database management easier

Signed-off-by: quexeky <git@quexeky.dev>

* feat: Download resuming

Signed-off-by: quexeky <git@quexeky.dev>

* feat: Resume button and PartiallyInstalled status

Signed-off-by: quexeky <git@quexeky.dev>

* feat: Download validation

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Ran cargo fix & cargo fmt

Signed-off-by: quexeky <git@quexeky.dev>

* fix: download validation, installs, etc

* chore: version bump

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-07-14 16:31:06 +10:00
2874b9776b fix: Accidentally moved request when setting the header
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-25 09:17:06 +10:00
afcd4e916f chore: bump version to 0.3.0-rc-4 2025-06-25 09:05:08 +10:00
885fa42ecc fix: Move Authorization header generation to download_game_chunk()
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-25 06:53:42 +10:00
6d295bd47f fix: Broken README path
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 06:37:14 +10:00
c3ee09af85 fix: Update broken README link in docs
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 06:35:57 +10:00
0ce55e12c5 fix: Re-update the user and app status when recieve_handshake is called (#54)
Also enabled assetProtocol for better caching in general

Signed-off-by: quexeky <git@quexeky.dev>
2025-06-06 12:09:44 +10:00
86bce1c68d Release: v0.3.0-rc-3 (#51) 2025-06-06 09:25:44 +10:00
60 changed files with 1496 additions and 801 deletions

23
.github/workflows/clippy.yml vendored Normal file
View File

@ -0,0 +1,23 @@
on: push
name: Clippy check
jobs:
clippy_check:
runs-on: ubuntu-24.04
permissions:
checks: write
steps:
- uses: actions/checkout@v1
- name: install dependencies (ubuntu only)
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path ./src-tauri/Cargo.toml

View File

@ -22,9 +22,9 @@ jobs:
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest' # for Intel based macs.
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'ubuntu-24.04-arm'
- platform: 'ubuntu-22.04-arm'
args: '--target aarch64-unknown-linux-gnu'
- platform: 'windows-latest'
args: ''
@ -48,12 +48,11 @@ jobs:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
- name: install frontend dependencies
run: yarn install # change this to npm, pnpm or bun depending on which one you use.

View File

@ -4,7 +4,7 @@ Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It u
## Running
Before setting up the drop app, be sure that you have a server set up.
The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html)
The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
## Current features
Currently supported are the following features:

View File

@ -1,78 +1,52 @@
<template>
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<div class="inline-flex divide-x divide-zinc-900">
<button
type="button"
@click="() => buttonActions[props.status.type]()"
:class="[
styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
styles[props.status.type],
showDropdown ? 'rounded-l-md' : 'rounded-md',
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]">
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
{{ buttonNames[props.status.type] }}
</button>
<Menu
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
<div class="h-full">
<MenuButton
:class="[
styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<MenuButton :class="[
styles[props.status.type],
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]">
<ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<MenuItems
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
>
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
<div class="py-1">
<MenuItem v-slot="{ active }">
<button
@click="() => emit('options')"
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]"
>
Options
<Cog6ToothIcon class="size-5" />
</button>
<button @click="() => emit('options')" :class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]">
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem>
<MenuItem v-slot="{ active }">
<button
@click="() => emit('uninstall')"
:class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]"
>
Uninstall
<TrashIcon class="size-5" />
</button>
<button @click="() => emit('uninstall')" :class="[
active
? 'bg-zinc-800 text-zinc-100 outline-none'
: 'text-zinc-400',
'w-full block px-4 py-2 text-sm inline-flex justify-between',
]">
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem>
</div>
</MenuItems>
@ -87,6 +61,7 @@ import {
ChevronDownIcon,
PlayIcon,
QueueListIcon,
StopIcon,
WrenchIcon,
} from "@heroicons/vue/20/solid";
@ -103,12 +78,14 @@ const emit = defineEmits<{
(e: "uninstall"): void;
(e: "kill"): void;
(e: "options"): void;
(e: "resume"): void
}>();
const showDropdown = computed(
() =>
props.status.type === GameStatusEnum.Installed ||
props.status.type === GameStatusEnum.SetupRequired
props.status.type === GameStatusEnum.SetupRequired ||
props.status.type === GameStatusEnum.PartiallyInstalled
);
const styles: { [key in GameStatusEnum]: string } = {
@ -128,6 +105,8 @@ const styles: { [key in GameStatusEnum]: string } = {
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Running]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.PartiallyInstalled]:
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
};
const buttonNames: { [key in GameStatusEnum]: string } = {
@ -139,6 +118,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop",
[GameStatusEnum.PartiallyInstalled]: "Resume"
};
const buttonIcons: { [key in GameStatusEnum]: Component } = {
@ -149,7 +129,8 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Installed]: PlayIcon,
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: PlayIcon,
[GameStatusEnum.Running]: StopIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
};
const buttonActions: { [key in GameStatusEnum]: () => void } = {
@ -159,7 +140,8 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => {},
[GameStatusEnum.Uninstalling]: () => { },
[GameStatusEnum.Running]: () => emit("kill"),
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
};
</script>

View File

@ -1,19 +1,30 @@
<template>
<div>
<div
class="relative mb-3 transition-transform duration-300 hover:scale-105 active:scale-95"
>
<div class="mb-3 inline-flex gap-x-2">
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
>
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<MagnifyingGlassIcon
class="h-5 w-5 text-zinc-400"
aria-hidden="true"
/>
</div>
<input
type="text"
v-model="searchQuery"
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search library..."
/>
</div>
<input
type="text"
v-model="searchQuery"
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
placeholder="Search library..."
/>
<button
@click="() => calculateGames(true)"
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
>
<ArrowPathIcon class="size-4" />
</button>
</div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
@ -60,7 +71,7 @@
</template>
<script setup lang="ts">
import { MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
import { TransitionGroup } from "vue";
@ -76,6 +87,7 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Updating]: "text-blue-500",
[GameStatusEnum.Uninstalling]: "text-zinc-100",
[GameStatusEnum.SetupRequired]: "text-yellow-500",
[GameStatusEnum.PartiallyInstalled]: "text-gray-600",
};
const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Remote]: "Not installed",
@ -86,6 +98,7 @@ const gameStatusText: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Uninstalling]: "Uninstalling...",
[GameStatusEnum.SetupRequired]: "Setup required",
[GameStatusEnum.Running]: "Running",
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
};
const router = useRouter();
@ -99,16 +112,20 @@ const icons: { [key: string]: string } = {};
const rawGames: Ref<Game[], Game[]> = ref([]);
async function calculateGames() {
rawGames.value = await invoke("fetch_library");
for (const game of rawGames.value) {
async function calculateGames(clearAll = false) {
if (clearAll) rawGames.value = [];
// If we update immediately, the navigation gets re-rendered before we
// add all the necessary state, and it freaks tf out
const newGames = await invoke<typeof rawGames.value>("fetch_library");
for (const game of newGames) {
if (games[game.id]) continue;
games[game.id] = await useGame(game.id);
}
for (const game of rawGames.value) {
for (const game of newGames) {
if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconObjectId);
}
rawGames.value = newGames;
}
await calculateGames();

View File

@ -14,7 +14,6 @@ export type SerializedGameStatus = [
];
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
console.log(status);
if (status[0]) {
return {
type: status[0].type,
@ -48,7 +47,6 @@ export const useGame = async (gameId: string) => {
status: SerializedGameStatus;
version?: GameVersion;
} = event.payload as any;
console.log(payload.status);
gameStatusRegistry[gameId].value = parseStatus(payload.status);
/**

View File

@ -1,9 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { data } from "autoprefixer";
import { AppStatus, type AppState } from "~/types";
export function setupHooks() {
const router = useRouter();
const state = useAppState();
listen("auth/processing", (event) => {
router.push("/auth/processing");
@ -15,8 +17,9 @@ export function setupHooks() {
);
});
listen("auth/finished", (event) => {
router.push("/store");
listen("auth/finished", async (event) => {
router.push("/library");
state.value = JSON.parse(await invoke("fetch_state"));
});
listen("download_error", (event) => {
@ -27,12 +30,31 @@ export function setupHooks() {
description: `Drop encountered an error while downloading your game: "${(
event.payload as unknown as string
).toString()}"`,
buttonText: "Close"
buttonText: "Close",
},
(e, c) => c()
);
});
// This is for errors that (we think) aren't our fault
listen("launch_external_error", (event) => {
createModal(
ModalType.Confirmation,
{
title: "Did something go wrong?",
description:
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
buttonText: "Open",
},
async (e, c) => {
if (e == "confirm") {
await invoke("open_process_logs", { gameId: event.payload });
}
c();
}
);
});
/*
document.addEventListener("contextmenu", (event) => {
@ -60,6 +82,6 @@ export function initialNavigation(state: Ref<AppState>) {
router.push("/error/serverunavailable");
break;
default:
router.push("/store");
router.push("/library");
}
}

View File

@ -1,7 +1,7 @@
{
"name": "drop-app",
"private": true,
"version": "0.3.0-rc-2",
"version": "0.3.0-rc-8",
"type": "module",
"scripts": {
"build": "nuxt build",
@ -18,6 +18,7 @@
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-opener": "^2.4.0",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-shell": "^2.2.1",
"koa": "^2.16.1",

View File

@ -32,6 +32,7 @@
@uninstall="() => uninstall()"
@kill="() => kill()"
@options="() => (configureModalOpen = true)"
@resume="() => resumeDownload()"
:status="status"
/>
<a
@ -495,6 +496,7 @@ const currentImageIndex = ref(0);
const configureModalOpen = ref(false);
async function installFlow() {
installFlowOpen.value = true;
versionOptions.value = undefined;
@ -532,6 +534,15 @@ async function install() {
installLoading.value = false;
}
async function resumeDownload() {
try {
await invoke("resume_download", { gameId: game.value.id })
}
catch(e) {
console.error(e)
}
}
async function launch() {
try {
await invoke("launch_game", { id: game.value.id });

View File

@ -91,7 +91,12 @@
<script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
// const actionNames = {
// [GameStatusEnum.Downloading]: "downloading",
// [GameStatusEnum.Verifying]: "verifying",
// }
const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => {

View File

@ -106,8 +106,6 @@ const systemData = await invoke<{
dataDir: string;
}>("fetch_system_data");
console.log(systemData);
clientId.value = systemData.clientId;
baseUrl.value = systemData.baseUrl;
dataDir.value = systemData.dataDir;

View File

@ -1,4 +1,37 @@
<template>
<iframe src="server://drop.local/store" class="w-full h-full" />
<div class="grow w-full h-full flex items-center justify-center">
<div class="flex flex-col items-center">
<BuildingStorefrontIcon
class="h-12 w-12 text-blue-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Store not supported in client
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-lg">
Currently, Drop requires you to view the store in your browser.
Please click the button below to open it in your default browser.
</p>
<NuxtLink
:href="storeUrl"
target="_blank"
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
>
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
BuildingStorefrontIcon,
} from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
</script>

257
src-tauri/Cargo.lock generated
View File

@ -68,6 +68,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ashpd"
version = "0.11.0"
@ -427,15 +433,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "binary-stream"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4ef03ef225ea9a0b680a5926a58cb45d8eb56abf23d8a8b5c5dbc61235e2dac"
dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "bincode"
version = "1.3.3"
@ -445,6 +442,30 @@ dependencies = [
"serde",
]
[[package]]
name = "bitcode"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf300f4aa6e66f3bdff11f1236a88c622fe47ea814524792240b4d554d9858ee"
dependencies = [
"arrayvec",
"bitcode_derive",
"bytemuck",
"glam",
"serde",
]
[[package]]
name = "bitcode_derive"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b6b4cb608b8282dc3b53d0f4c9ab404655d562674c682db7e6c0458cc83c23"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -538,9 +559,9 @@ checksum = "66bb12751a83493ef4b8da1120451a262554e216a247f14b48cb5e8fe7ed8bdf"
[[package]]
name = "brotli"
version = "7.0.0"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -549,9 +570,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "4.0.3"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -946,15 +967,15 @@ dependencies = [
[[package]]
name = "cssparser"
version = "0.27.2"
version = "0.29.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa"
dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 0.4.8",
"itoa",
"matches",
"phf 0.8.0",
"phf 0.10.1",
"proc-macro2",
"quote",
"smallvec",
@ -1247,9 +1268,10 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.3.0-rc-2"
version = "0.3.0-rc-8"
dependencies = [
"atomic-instant-full",
"bitcode",
"boxcar",
"cacache 13.1.0",
"chrono",
@ -1278,7 +1300,6 @@ dependencies = [
"rustix 0.38.44",
"schemars",
"serde",
"serde-binary",
"serde_json",
"serde_with",
"sha1",
@ -1290,6 +1311,7 @@ dependencies = [
"tauri-plugin-autostart",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
@ -1906,6 +1928,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "glam"
version = "0.30.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
[[package]]
name = "glib"
version = "0.18.5"
@ -2131,16 +2159,14 @@ dependencies = [
[[package]]
name = "html5ever"
version = "0.26.0"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn 1.0.109",
"match_token",
]
[[package]]
@ -2151,7 +2177,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.15",
"itoa",
]
[[package]]
@ -2162,7 +2188,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.15",
"itoa",
]
[[package]]
@ -2212,6 +2238,12 @@ dependencies = [
"serde",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "http-serde"
version = "1.1.3"
@ -2265,7 +2297,7 @@ dependencies = [
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa 1.0.15",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
@ -2287,7 +2319,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"httparse",
"itoa 1.0.15",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
@ -2557,12 +2589,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.15"
@ -2678,14 +2704,13 @@ dependencies = [
[[package]]
name = "kuchikiki"
version = "0.8.2"
version = "0.8.8-speedreader"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
dependencies = [
"cssparser",
"html5ever",
"indexmap 1.9.3",
"matches",
"indexmap 2.9.0",
"selectors",
]
@ -2850,18 +2875,29 @@ dependencies = [
[[package]]
name = "markup5ever"
version = "0.11.0"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
dependencies = [
"log",
"phf 0.10.1",
"phf_codegen 0.10.0",
"phf 0.11.3",
"phf_codegen 0.11.3",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "match_token"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "matches"
version = "0.1.10"
@ -2976,9 +3012,9 @@ dependencies = [
[[package]]
name = "muda"
version = "0.16.1"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492"
checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988"
dependencies = [
"crossbeam-channel",
"dpi",
@ -3582,9 +3618,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
[[package]]
@ -3593,7 +3627,9 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_macros 0.10.0",
"phf_shared 0.10.0",
"proc-macro-hack",
]
[[package]]
@ -3618,12 +3654,12 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.10.0"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"phf_generator 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
@ -3658,12 +3694,12 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"proc-macro-hack",
"proc-macro2",
"quote",
@ -4552,22 +4588,20 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "selectors"
version = "0.22.0"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
dependencies = [
"bitflags 1.3.2",
"cssparser",
"derive_more",
"fxhash",
"log",
"matches",
"phf 0.8.0",
"phf_codegen 0.8.0",
"precomputed-hash",
"servo_arc",
"smallvec",
"thin-slice",
]
[[package]]
@ -4588,17 +4622,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-binary"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b550db407b83ed53a4f76f888bfd7441b685abc2c086e20fb47781a286940506"
dependencies = [
"binary-stream",
"serde",
"thiserror 1.0.69",
]
[[package]]
name = "serde-untagged"
version = "0.1.7"
@ -4648,7 +4671,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa 1.0.15",
"itoa",
"memchr",
"ryu",
"serde",
@ -4681,7 +4704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa 1.0.15",
"itoa",
"ryu",
"serde",
]
@ -4723,7 +4746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.9.0",
"itoa 1.0.15",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
@ -4753,9 +4776,9 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.1.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741"
dependencies = [
"nodrop",
"stable_deref_trait",
@ -5156,9 +5179,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.33.0"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.1",
@ -5232,21 +5255,21 @@ dependencies = [
[[package]]
name = "tauri"
version = "2.5.1"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc"
checksum = "124e129c9c0faa6bec792c5948c89e86c90094133b0b9044df0ce5f0a8efaa0d"
dependencies = [
"anyhow",
"bytes",
"dirs 6.0.0",
"dunce",
"embed_plist",
"futures-util",
"getrandom 0.2.16",
"getrandom 0.3.3",
"glob",
"gtk",
"heck 0.5.0",
"http 1.3.1",
"http-range",
"jni",
"libc",
"log",
@ -5283,9 +5306,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc"
checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83"
dependencies = [
"anyhow",
"cargo_toml",
@ -5305,9 +5328,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47"
checksum = "f5df493a1075a241065bc865ed5ef8d0fbc1e76c7afdc0bf0eccfaa7d4f0e406"
dependencies = [
"base64 0.22.1",
"brotli",
@ -5332,9 +5355,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.2.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112"
checksum = "f237fbea5866fa5f2a60a21bea807a2d6e0379db070d89c3a10ac0f2d4649bbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -5346,9 +5369,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601"
checksum = "1d9a0bd00bf1930ad1a604d08b0eb6b2a9c1822686d65d7f4731a7723b8901d3"
dependencies = [
"anyhow",
"glob",
@ -5435,6 +5458,28 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation 0.3.1",
"open",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"url",
"windows",
"zbus",
]
[[package]]
name = "tauri-plugin-os"
version = "2.2.1"
@ -5492,9 +5537,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39"
checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4"
dependencies = [
"cookie",
"dpi",
@ -5514,9 +5559,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.6.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2"
checksum = "902b5aa9035e16f342eb64f8bf06ccdc2808e411a2525ed1d07672fa4e780bad"
dependencies = [
"gtk",
"http 1.3.1",
@ -5541,9 +5586,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4"
checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e"
dependencies = [
"anyhow",
"brotli",
@ -5612,12 +5657,6 @@ dependencies = [
"utf-8",
]
[[package]]
name = "thin-slice"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.69"
@ -5687,7 +5726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa 1.0.15",
"itoa",
"num-conv",
"powerfmt",
"serde",
@ -5938,9 +5977,9 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.20.1"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0"
checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a"
dependencies = [
"crossbeam-channel",
"dirs 6.0.0",
@ -6431,9 +6470,9 @@ dependencies = [
[[package]]
name = "webview2-com"
version = "0.37.0"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601"
checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
@ -6456,9 +6495,9 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
version = "0.37.0"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.12",
"windows",
@ -6931,9 +6970,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wry"
version = "0.51.2"
version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2"
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
dependencies = [
"base64 0.22.1",
"block2 0.6.1",

View File

@ -1,9 +1,9 @@
[package]
name = "drop-app"
version = "0.3.0-rc-2"
version = "0.3.0-rc-8"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -25,7 +25,6 @@ tauri-build = { version = "2.0.0", features = [] }
[dependencies]
tauri-plugin-shell = "2.2.1"
serde_json = "1"
serde-binary = "0.5.0"
rayon = "1.10.0"
webbrowser = "1.0.2"
url = "2.5.2"
@ -67,6 +66,8 @@ filetime = "0.2.25"
walkdir = "2.5.0"
known-folders = "1.2.0"
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
# tailscale = { path = "./tailscale" }
[dependencies.dynfmt]
@ -75,7 +76,7 @@ features = ["curly"]
[dependencies.tauri]
version = "2.1.1"
features = ["tray-icon"]
features = ["protocol-asset", "tray-icon"]
[dependencies.tokio]
version = "1.40.0"

View File

@ -14,6 +14,7 @@
"core:window:allow-close",
"deep-link:default",
"dialog:default",
"os:default"
"os:default",
"opener:default"
]
}

View File

@ -1,4 +1,4 @@
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use log::debug;
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
@ -17,7 +17,6 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
let mut db_handle = borrow_db_mut_checked();
db_handle.settings.autostart = enabled;
drop(db_handle);
save_db();
Ok(())
}

View File

@ -16,7 +16,7 @@ pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mut
Ok(_) => debug!("download manager terminated correctly"),
Err(_) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{:?}", e),
Err(e) => panic!("{e:?}"),
}
app.exit(0);

View File

@ -1,3 +1,3 @@
pub mod autostart;
pub mod cleanup;
pub mod commands;
pub mod commands;

View File

@ -6,10 +6,12 @@ use std::{
use serde_json::Value;
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
use crate::{
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
};
use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
db::{borrow_db_checked, DATA_ROOT_DIR},
debug::SystemData,
models::data::Settings,
};
@ -26,8 +28,6 @@ pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
pub fn delete_download_dir(index: usize) {
let mut lock = borrow_db_mut_checked();
lock.applications.install_dirs.remove(index);
drop(lock);
save_db();
}
#[tauri::command]
@ -58,7 +58,6 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
}
lock.applications.install_dirs.push(new_dir);
drop(lock);
save_db();
Ok(())
}
@ -72,8 +71,6 @@ pub fn update_settings(new_settings: Value) {
}
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings;
drop(db_lock);
save_db();
}
#[tauri::command]
pub fn fetch_settings() -> Settings {
@ -85,7 +82,7 @@ pub fn fetch_system_data() -> SystemData {
SystemData::new(
db_handle.auth.as_ref().unwrap().client_id.clone(),
db_handle.base_url.clone(),
DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
DATA_ROOT_DIR.to_string_lossy().to_string(),
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
)
}

View File

@ -1,7 +1,9 @@
use std::{
fs::{self, create_dir_all},
mem::ManuallyDrop,
ops::{Deref, DerefMut},
path::PathBuf,
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
};
use chrono::Utc;
@ -15,9 +17,8 @@ use crate::DB;
use super::models::data::Database;
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop")));
// Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)]
@ -27,15 +28,16 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::rmp_serde_1_3::RmpSerde::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
native_model::rmp_serde_1_3::RmpSerde::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string()))
}
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new();
s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let val =
native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
.map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
}
}
@ -50,15 +52,14 @@ pub trait DatabaseImpls {
}
impl DatabaseImpls for DatabaseInterface {
fn set_up_database() -> DatabaseInterface {
let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
let db_path = data_root_dir.join("drop.db");
let games_base_dir = data_root_dir.join("games");
let logs_root_dir = data_root_dir.join("logs");
let cache_dir = data_root_dir.join("cache");
let pfx_dir = data_root_dir.join("pfx");
let db_path = DATA_ROOT_DIR.join("drop.db");
let games_base_dir = DATA_ROOT_DIR.join("games");
let logs_root_dir = DATA_ROOT_DIR.join("logs");
let cache_dir = DATA_ROOT_DIR.join("cache");
let pfx_dir = DATA_ROOT_DIR.join("pfx");
debug!("creating data directory at {:?}", data_root_dir);
create_dir_all(data_root_dir.clone()).unwrap();
debug!("creating data directory at {DATA_ROOT_DIR:?}");
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap();
create_dir_all(&games_base_dir).unwrap();
create_dir_all(&logs_root_dir).unwrap();
create_dir_all(&cache_dir).unwrap();
@ -100,17 +101,14 @@ fn handle_invalid_database(
games_base_dir: PathBuf,
cache_dir: PathBuf,
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
warn!("{}", _e);
warn!("{_e}");
let new_path = {
let time = Utc::now().timestamp();
let mut base = db_path.clone();
base.set_file_name(format!("drop.db.backup-{}", time));
base.set_file_name(format!("drop.db.backup-{time}"));
base
};
info!(
"old database stored at: {}",
new_path.to_string_lossy().to_string()
);
info!("old database stored at: {}", new_path.to_string_lossy());
fs::rename(&db_path, &new_path).unwrap();
let db = Database::new(
@ -122,32 +120,59 @@ fn handle_invalid_database(
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
}
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
// To automatically save the database upon drop
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = RwLockWriteGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = RwLockReadGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Drop for DBWrite<'a> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {e}");
panic!("database failed to save with error {e}")
}
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => data,
Ok(data) => DBRead(data),
Err(e) => {
error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {}", e);
error!("database borrow failed with error {e}");
panic!("database borrow failed with error {e}");
}
}
}
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() {
Ok(data) => data,
Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => {
error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {}", e);
}
}
}
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
error!("database borrow mut failed with error {e}");
panic!("database borrow mut failed with error {e}");
}
}
}

View File

@ -1,19 +1,27 @@
use crate::database::models::data::Database;
pub mod data {
use native_model::{native_model, Model};
use std::path::PathBuf;
use native_model::native_model;
use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion;
pub type Database = v2::Database;
pub type Database = v3::Database;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus;
pub type GameDownloadStatus = v2::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v1::DatabaseApplications;
pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::{collections::HashMap, process::Command};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
pub mod v1 {
use crate::process::process_manager::Platform;
use serde_with::serde_as;
@ -102,7 +110,7 @@ pub mod data {
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
@ -126,7 +134,7 @@ pub mod data {
pub enum DownloadType {
Game,
Tool,
DLC,
Dlc,
Mod,
}
@ -162,14 +170,122 @@ pub mod data {
}
pub mod v2 {
use std::{collections::HashMap, path::PathBuf, process::Command};
use std::{collections::HashMap, path::PathBuf};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
use serde_with::serde_as;
use super::*;
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: v1::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
pub umu_installed: bool,
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: crate::database::models::Database::create_new_compat_info(),
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
fn from(value: v1::GameDownloadStatus) -> Self {
match value {
v1::GameDownloadStatus::Remote {} => Self::Remote {},
v1::GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Self::SetupRequired {
version_name,
install_dir,
},
v1::GameDownloadStatus::Installed {
version_name,
install_dir,
} => Self::Installed {
version_name,
install_dir,
},
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
Self {
game_statuses: value
.game_statuses
.into_iter()
.map(|x| (x.0, x.1.into()))
.collect::<HashMap<String, GameDownloadStatus>>(),
install_dirs: value.install_dirs,
game_versions: value.game_versions,
installed_game_version: value.installed_game_version,
transient_statuses: value.transient_statuses,
}
}
}
}
mod v3 {
use std::path::PathBuf;
use super::*;
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
@ -181,55 +297,13 @@ pub mod data {
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
umu_installed: bool,
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
impl From<v2::Database> for Database {
fn from(value: v2::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
applications: value.applications.into(),
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: Database::create_new_compat_info(),
@ -237,4 +311,37 @@ pub mod data {
}
}
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
}

View File

@ -1,10 +1,10 @@
use std::{
collections::HashMap,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex,
mpsc::{Receiver, Sender, channel},
},
thread::{spawn, JoinHandle},
thread::{JoinHandle, spawn},
};
use log::{debug, error, info, warn};
@ -17,8 +17,13 @@ use crate::{
};
use super::{
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
downloadable::Downloadable, util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue},
download_manager_frontend::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
downloadable::Downloadable,
util::{
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
progress_object::ProgressObject,
queue::Queue,
},
};
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
@ -171,7 +176,6 @@ impl DownloadManagerBuilder {
DownloadManagerSignal::Cancel(meta) => {
self.manage_cancel_signal(&meta);
}
_ => {}
};
}
}
@ -179,7 +183,7 @@ impl DownloadManagerBuilder {
debug!("got signal Queue");
let meta = download_agent.metadata();
debug!("queue metadata: {:?}", meta);
debug!("queue metadata: {meta:?}");
if self.download_queue.exists(meta.clone()) {
warn!("download with same ID already exists");
@ -205,16 +209,15 @@ impl DownloadManagerBuilder {
return;
}
if self.current_download_agent.is_some() {
if self.download_queue.read().front().unwrap()
if self.current_download_agent.is_some()
&& self.download_queue.read().front().unwrap()
== &self.current_download_agent.as_ref().unwrap().metadata()
{
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
{
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
debug!("current download queue: {:?}", self.download_queue.read());
@ -222,7 +225,7 @@ impl DownloadManagerBuilder {
// Should always be Some if the above two statements keep going
let agent_data = self.download_queue.read().front().unwrap().clone();
info!("starting download for {:?}", agent_data);
info!("starting download for {agent_data:?}");
let download_agent = self
.download_agent_registry
@ -243,13 +246,30 @@ impl DownloadManagerBuilder {
// Ok(true) is for completed and exited properly
Ok(true) => {
debug!("download {:?} has completed", download_agent.metadata());
download_agent.on_complete(&app_handle);
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
match download_agent.validate() {
Ok(true) => {
download_agent.on_complete(&app_handle);
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
}
Ok(false) => {
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!(
"download {:?} has validation error {}",
download_agent.metadata(),
&e
);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
}
// Ok(false) is for incomplete but exited properly
Ok(false) => {
debug!("Donwload agent finished incomplete");
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
@ -275,11 +295,12 @@ impl DownloadManagerBuilder {
}
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
debug!("got signal Completed");
if let Some(interface) = &self.current_download_agent {
if interface.metadata() == meta {
self.remove_and_cleanup_front_download(&meta);
}
if let Some(interface) = &self.current_download_agent
&& interface.metadata() == meta
{
self.remove_and_cleanup_front_download(&meta);
}
self.push_ui_queue_update();
self.sender.send(DownloadManagerSignal::Go).unwrap();
}
@ -291,7 +312,7 @@ impl DownloadManagerBuilder {
self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata());
}
self.set_status(DownloadManagerStatus::Error(error));
self.set_status(DownloadManagerStatus::Error);
}
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
debug!("got signal Cancel");

View File

@ -18,7 +18,8 @@ use crate::{
};
use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent}, util::queue::Queue,
download_manager_builder::{CurrentProgressObject, DownloadAgent},
util::queue::Queue,
};
pub enum DownloadManagerSignal {
@ -37,16 +38,11 @@ pub enum DownloadManagerSignal {
Finish,
/// Stops, removes, and tells a download to cleanup
Cancel(DownloadableMetadata),
/// Removes a given application
Remove(DownloadableMetadata),
/// Any error which occurs in the agent
Error(ApplicationDownloadError),
/// Pushes UI update
UpdateUIQueue,
UpdateUIStats(usize, usize), //kb/s and seconds
/// Uninstall download
/// Takes download ID
Uninstall(DownloadableMetadata),
}
#[derive(Debug)]
@ -54,8 +50,7 @@ pub enum DownloadManagerStatus {
Downloading,
Paused,
Empty,
Error(ApplicationDownloadError),
Finished,
Error,
}
impl Serialize for DownloadManagerStatus {
@ -63,7 +58,7 @@ impl Serialize for DownloadManagerStatus {
where
S: serde::Serializer,
{
serializer.serialize_str(&format!["{:?}", self])
serializer.serialize_str(&format!["{self:?}"])
}
}
@ -71,6 +66,7 @@ impl Serialize for DownloadManagerStatus {
pub enum DownloadStatus {
Queued,
Downloading,
Validating,
Error,
}
@ -152,10 +148,7 @@ impl DownloadManager {
.unwrap();
}
debug!(
"moving download at index {} to index {}",
current_index, new_index
);
debug!("moving download at index {current_index} to index {new_index}");
let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap();
@ -185,11 +178,6 @@ impl DownloadManager {
let terminator = self.terminator.lock().unwrap().take();
terminator.unwrap().join()
}
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
self.command_sender
.send(DownloadManagerSignal::Uninstall(meta))
.unwrap();
}
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
self.command_sender.clone()
}

View File

@ -8,13 +8,15 @@ use crate::{
};
use super::{
download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
download_manager_frontend::DownloadStatus,
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
};
pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl;
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle);

View File

@ -1,5 +1,5 @@
pub mod commands;
pub mod download_manager;
pub mod download_manager_builder;
pub mod download_manager_frontend;
pub mod downloadable;
pub mod util;
pub mod util;

View File

@ -1,4 +1,4 @@
pub mod download_thread_control_flag;
pub mod progress_object;
pub mod queue;
pub mod rolling_progress_updates;
pub mod download_thread_control_flag;

View File

@ -10,11 +10,9 @@ use std::{
use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle;
use crate::download_manager::download_manager::DownloadManagerSignal;
use crate::download_manager::download_manager_frontend::DownloadManagerSignal;
use super::{
rolling_progress_updates::RollingProgressWindow,
};
use super::rolling_progress_updates::RollingProgressWindow;
#[derive(Clone)]
pub struct ProgressObject {
@ -86,6 +84,17 @@ impl ProgressObject {
.map(|instance| instance.load(Ordering::Relaxed))
.sum()
}
pub fn reset(&self, size: usize) {
self.set_time_now();
self.set_size(size);
self.bytes_last_update.store(0, Ordering::Release);
self.rolling.reset();
self.progress_instances
.lock()
.unwrap()
.iter()
.for_each(|x| x.store(0, Ordering::Release));
}
pub fn get_max(&self) -> usize {
*self.max.lock().unwrap()
}
@ -124,7 +133,7 @@ pub fn calculate_update(progress: &ProgressObject) {
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
let bytes_remaining = max - current_bytes_downloaded; // bytes
let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes
progress.update_window(kilobytes_per_second);
push_update(progress, bytes_remaining);

View File

@ -32,49 +32,13 @@ impl Queue {
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
self.edit().pop_front()
}
pub fn is_empty(&self) -> bool {
self.inner.lock().unwrap().len() == 0
}
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
self.read().contains(&meta)
}
/// Either inserts `interface` at the specified index, or appends to
/// the back of the deque if index is greater than the length of the deque
pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
if self.read().len() > index {
self.append(interface);
} else {
self.edit().insert(index, interface);
}
}
pub fn append(&self, interface: DownloadableMetadata) {
self.edit().push_back(interface);
}
pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option<DownloadableMetadata> {
let mut queue = self.edit();
let front = queue.front()?;
if front == meta {
return queue.pop_front();
}
None
}
pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option<usize> {
self.read().iter().position(|data| data == meta)
}
pub fn move_to_index_by_meta(
&self,
meta: &DownloadableMetadata,
new_index: usize,
) -> Result<(), ()> {
let index = match self.get_by_meta(meta) {
Some(index) => index,
None => return Err(()),
};
let existing = match self.edit().remove(index) {
Some(existing) => existing,
None => return Err(()),
};
self.edit().insert(new_index, existing);
Ok(())
}
}

View File

@ -30,4 +30,9 @@ impl<const S: usize> RollingProgressWindow<S> {
.sum::<usize>()
/ S
}
pub fn reset(&self) {
self.window
.iter()
.for_each(|x| x.store(0, Ordering::Release));
}
}

View File

@ -5,14 +5,13 @@ use std::{
use serde_with::SerializeDisplay;
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
use super::remote_access_error::RemoteAccessError;
// TODO: Rename / separate from downloads
#[derive(Debug, SerializeDisplay)]
pub enum ApplicationDownloadError {
Communication(RemoteAccessError),
Checksum,
Setup(SetupError),
Lock,
IoError(io::ErrorKind),
DownloadError,
@ -21,11 +20,10 @@ pub enum ApplicationDownloadError {
impl Display for ApplicationDownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
}
}

View File

@ -1,21 +0,0 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(Debug, SerializeDisplay, Clone, Copy)]
pub enum BackupError {
InvalidSystem,
NotFound,
ParseError
}
impl Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
BackupError::InvalidSystem => "Attempted to generate path for invalid system",
BackupError::NotFound => "Could not generate or find path",
BackupError::ParseError => "Failed to parse path",
};
write!(f, "{}", s)
}
}

View File

@ -10,8 +10,8 @@ pub enum DownloadManagerError<T> {
impl<T> Display for DownloadManagerError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadManagerError::IOError(error) => write!(f, "{}", error),
DownloadManagerError::SignalError(send_error) => write!(f, "{}", send_error),
DownloadManagerError::IOError(error) => write!(f, "{error}"),
DownloadManagerError::SignalError(send_error) => write!(f, "{send_error}"),
}
}
}

View File

@ -5,6 +5,6 @@ use serde::Deserialize;
pub struct DropServerError {
pub status_code: usize,
pub status_message: String,
pub message: String,
pub url: String,
// pub message: String,
// pub url: String,
}

View File

@ -11,8 +11,7 @@ impl Display for LibraryError {
match self {
LibraryError::MetaNotFound(id) => write!(
f,
"Could not locate any installed version of game ID {} in the database",
id
"Could not locate any installed version of game ID {id} in the database"
),
}
}

View File

@ -1,8 +1,6 @@
pub mod application_download_error;
pub mod drop_server_error;
pub mod download_manager_error;
pub mod drop_server_error;
pub mod library_error;
pub mod process_error;
pub mod remote_access_error;
pub mod setup_error;
pub mod backup_error;

View File

@ -13,6 +13,7 @@ pub enum ProcessError {
IOError(Error),
FormatError(String), // String errors supremacy
InvalidPlatform,
OpenerError(tauri_plugin_opener::Error)
}
impl Display for ProcessError {
@ -22,12 +23,13 @@ impl Display for ProcessError {
ProcessError::NotInstalled => "Game not installed",
ProcessError::AlreadyRunning => "Game already running",
ProcessError::NotDownloaded => "Game not downloaded",
ProcessError::InvalidID => "Invalid Game ID",
ProcessError::InvalidVersion => "Invalid Game version",
ProcessError::InvalidID => "Invalid game ID",
ProcessError::InvalidVersion => "Invalid game version",
ProcessError::IOError(error) => &error.to_string(),
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
ProcessError::FormatError(e) => &format!("Failed to format template: {}", e),
};
write!(f, "{}", s)
ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
ProcessError::FormatError(e) => &format!("Failed to format template: {e}"),
ProcessError::OpenerError(error) => &format!("Failed to open directory: {error}"),
};
write!(f, "{s}")
}
}

View File

@ -18,7 +18,7 @@ pub enum RemoteAccessError {
HandshakeFailed(String),
GameNotFound(String),
InvalidResponse(DropServerError),
InvalidRedirect,
UnparseableResponse(String),
ManifestDownloadFailed(StatusCode, String),
OutOfSync,
Cache(cacache::Error),
@ -44,20 +44,19 @@ impl Display for RemoteAccessError {
)
},
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{}", parse_error)
write!(f, "{parse_error}")
}
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {}", id),
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {message}"),
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {id}"),
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {}, {}", error.status_code, error.status_message),
RemoteAccessError::UnparseableResponse(error) => write!(f, "server returned an invalid response: {error}"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
f,
"failed to download game manifest: {} {}",
status, response
"failed to download game manifest: {status} {response}"
),
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {error}"),
}
}
}

View File

@ -1,14 +0,0 @@
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
pub enum SetupError {
Context,
}
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Context => write!(f, "failed to generate contexts for download"),
}
}
}

View File

@ -41,7 +41,7 @@ pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError>
let client = Client::new();
let base_url = DB.fetch_base_url();
let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?;
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
let response = client
.post(base_url)

View File

@ -1,9 +1,15 @@
use std::sync::{Arc, Mutex};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use crate::{
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
download_manager::{
download_manager::DownloadManagerSignal, downloadable::Downloadable,
}, error::download_manager_error::DownloadManagerError, AppState
download_manager_frontend::DownloadManagerSignal, downloadable::Downloadable,
},
error::download_manager_error::DownloadManagerError,
AppState,
};
use super::download_agent::GameDownloadAgent;
@ -16,7 +22,7 @@ pub fn download_game(
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
game_id,
game_version,
install_dir,
@ -28,3 +34,39 @@ pub fn download_game(
.download_manager
.queue_download(game_download_agent)?)
}
#[tauri::command]
pub fn resume_download(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
let s = borrow_db_checked()
.applications
.game_statuses
.get(&game_id)
.unwrap()
.clone();
let (version_name, install_dir) = match s {
GameDownloadStatus::Remote {} => unreachable!(),
GameDownloadStatus::SetupRequired { .. } => unreachable!(),
GameDownloadStatus::Installed { .. } => unreachable!(),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
};
let sender = state.lock().unwrap().download_manager.get_sender();
let parent_dir: PathBuf = install_dir.into();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
version_name.clone(),
parent_dir.parent().unwrap().to_path_buf(),
sender,
)) as Box<dyn Downloadable + Send + Sync>);
Ok(state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)?)
}

View File

@ -1,23 +1,25 @@
use crate::auth::generate_authorization_header;
use crate::database::db::borrow_db_checked;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
ApplicationTransientStatus, DownloadType, DownloadableMetadata,
};
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_manager_frontend::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
use crate::download_manager::util::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
use crate::games::downloads::validate::game_validate_logic;
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
use crate::remote::requests::make_request;
use crate::DB;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
use slice_deque::SliceDeque;
use std::fs::{create_dir_all, File};
use std::path::Path;
use std::collections::HashMap;
use std::fs::{create_dir_all, OpenOptions};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::time::Instant;
@ -27,40 +29,48 @@ use tauri::{AppHandle, Emitter};
use rustix::fs::{fallocate, FallocateFlags};
use super::download_logic::download_game_chunk;
use super::stored_manifest::StoredManifest;
use super::drop_data::DropData;
pub struct GameDownloadAgent {
pub id: String,
pub version: String,
pub control_flag: DownloadThreadControl,
contexts: Mutex<Vec<DropDownloadContext>>,
completed_contexts: Mutex<SliceDeque<usize>>,
context_map: Mutex<HashMap<String, bool>>,
pub manifest: Mutex<Option<DropManifest>>,
pub progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
pub stored_manifest: StoredManifest,
pub stored_manifest: DropData,
status: Mutex<DownloadStatus>,
}
impl GameDownloadAgent {
pub fn new(
pub fn new_from_index(
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 = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock);
Self::new(id, version, base_dir, sender)
}
pub fn new(
id: String,
version: String,
base_dir: PathBuf,
sender: Sender<DownloadManagerSignal>,
) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
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());
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
Self {
id,
@ -68,7 +78,7 @@ impl GameDownloadAgent {
control_flag,
manifest: Mutex::new(None),
contexts: Mutex::new(Vec::new()),
completed_contexts: Mutex::new(SliceDeque::new()),
context_map: Mutex::new(HashMap::new()),
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender,
stored_manifest,
@ -173,11 +183,15 @@ impl GameDownloadAgent {
}
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
if !self.contexts.lock().unwrap().is_empty() {
return Ok(());
if self.contexts.lock().unwrap().is_empty() {
self.generate_contexts()?;
}
self.generate_contexts()?;
self.context_map
.lock()
.unwrap()
.extend(self.stored_manifest.get_contexts());
Ok(())
}
@ -189,20 +203,19 @@ impl GameDownloadAgent {
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_from_slice(&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 file = OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.create(true)
.open(path.clone())
.unwrap();
let mut running_offset = 0;
for (index, length) in chunk.lengths.iter().enumerate() {
@ -225,6 +238,14 @@ impl GameDownloadAgent {
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
}
}
let existing_contexts = self.stored_manifest.get_completed_contexts();
self.stored_manifest.set_contexts(
&contexts
.iter()
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
.collect::<Vec<(String, bool)>>(),
);
*self.contexts.lock().unwrap() = contexts;
Ok(())
@ -243,13 +264,14 @@ impl GameDownloadAgent {
.build()
.unwrap();
let completed_indexes = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_indexes.clone();
let completed_contexts = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_contexts.clone();
let contexts = self.contexts.lock().unwrap();
debug!("{:#?}", contexts);
debug!("{contexts:#?}");
pool.scope(|scope| {
let client = &reqwest::blocking::Client::new();
let context_map = self.context_map.lock().unwrap();
for (index, context) in contexts.iter().enumerate() {
let client = client.clone();
let completed_indexes = completed_indexes_loop_arc.clone();
@ -258,7 +280,7 @@ impl GameDownloadAgent {
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) {
if Some(&true) == context_map.get(&context.checksum) {
progress_handle.skip(context.length);
continue;
}
@ -274,7 +296,7 @@ impl GameDownloadAgent {
("name", &context.file_name),
("chunk", &context.index.to_string()),
],
|r| r.header("Authorization", generate_authorization_header()),
|r| r,
) {
Ok(request) => request,
Err(e) => {
@ -290,13 +312,12 @@ impl GameDownloadAgent {
scope.spawn(move |_| {
match download_game_chunk(context, &self.control_flag, progress_handle, request)
{
Ok(res) => {
if res {
completed_indexes.push(index);
}
Ok(true) => {
completed_indexes.push(context.checksum.clone());
}
Ok(false) => {}
Err(e) => {
error!("{}", e);
error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
@ -304,36 +325,42 @@ impl GameDownloadAgent {
}
});
let newly_completed = completed_indexes.to_owned();
let newly_completed = completed_contexts.to_owned();
let completed_lock_len = {
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
let mut context_map_lock = self.context_map.lock().unwrap();
for (_, item) in newly_completed.iter() {
completed_contexts_lock.push_front(*item);
context_map_lock.insert(item.clone(), true);
}
completed_contexts_lock.len()
context_map_lock.values().filter(|x| **x).count()
};
let context_map_lock = self.context_map.lock().unwrap();
let contexts = contexts
.iter()
.map(|x| {
(
x.checksum.clone(),
context_map_lock.get(&x.checksum).cloned().unwrap_or(false),
)
})
.collect::<Vec<(String, bool)>>();
drop(context_map_lock);
// If we're not out of contexts, we're not done, so we don't fire completed
if completed_lock_len != contexts.len() {
self.stored_manifest.set_contexts(&contexts);
self.stored_manifest.write();
// If there are any contexts left which are false
if !contexts.iter().all(|x| x.1) {
info!(
"download agent for {} exited without completing ({}/{})",
self.id.clone(),
completed_lock_len,
contexts.len(),
);
self.stored_manifest
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
self.stored_manifest.write();
return Ok(false);
}
// We've completed
self.sender
.send(DownloadManagerSignal::Completed(self.metadata()))
.unwrap();
Ok(true)
}
}
@ -370,9 +397,9 @@ impl Downloadable for GameDownloadAgent {
.emit("download_error", error.to_string())
.unwrap();
error!("error while managing download: {}", error);
error!("error while managing download: {error}");
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
handle
.applications
.transient_statuses
@ -390,18 +417,12 @@ impl Downloadable for GameDownloadAgent {
// 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) {
let meta = self.metadata();
*self.status.lock().unwrap() = DownloadStatus::Queued;
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None),
version: None,
},
)
.unwrap();
on_game_incomplete(
&self.metadata(),
self.stored_manifest.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
}
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
@ -409,4 +430,15 @@ impl Downloadable for GameDownloadAgent {
fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone()
}
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating;
game_validate_logic(
&self.stored_manifest,
self.contexts.lock().unwrap().clone(),
self.progress.clone(),
self.sender.clone(),
&self.control_flag,
)
}
}

View File

@ -1,14 +1,18 @@
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
use crate::download_manager::util::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::util::progress_object::ProgressHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::drop_server_error::DropServerError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext;
use log::warn;
use crate::remote::auth::generate_authorization_header;
use log::{debug, warn};
use md5::{Context, Digest};
use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions};
use std::io::{ErrorKind, Read};
use std::io::Read;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
@ -37,12 +41,9 @@ impl DropWriter<File> {
// 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.hasher
.write_all(buf)
.map_err(|e| io::Error::other(format!("Unable to write to hasher: {e}")))?;
self.destination.write(buf)
}
@ -90,19 +91,32 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
let mut current_size = 0;
loop {
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
buf_writer.flush()?;
return Ok(false);
}
let bytes_read = self.source.read(&mut copy_buf)?;
let mut bytes_read = self.source.read(&mut copy_buf)?;
current_size += bytes_read;
if current_size > self.size {
let over = current_size - self.size;
warn!("server sent too many bytes... {over} over");
bytes_read -= over;
current_size = self.size;
}
buf_writer.write_all(&copy_buf[0..bytes_read])?;
self.progress.add(bytes_read);
if current_size == self.size {
if current_size >= self.size {
debug!(
"finished with final size of {} vs {}",
current_size, self.size
);
break;
}
}
buf_writer.flush()?;
Ok(true)
}
@ -124,15 +138,21 @@ pub fn download_game_chunk(
progress.set(0);
return Ok(false);
}
let response = request
.header("Authorization", generate_authorization_header())
.send()
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
let err = response.json().unwrap();
debug!("chunk request got status code: {}", response.status());
let raw_res = response.text().unwrap();
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err),
));
};
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err),
RemoteAccessError::UnparseableResponse(raw_res),
));
}
@ -152,13 +172,14 @@ pub fn download_game_chunk(
));
}
let mut pipeline = DropDownloadPipeline::new(
response,
destination,
control_flag,
progress,
content_length.unwrap().try_into().unwrap(),
);
let length = content_length.unwrap().try_into().unwrap();
if length != ctx.length {
return Err(ApplicationDownloadError::DownloadError);
}
let mut pipeline =
DropDownloadPipeline::new(response, destination, control_flag, progress, length);
let completed = pipeline
.copy()
@ -183,5 +204,10 @@ pub fn download_game_chunk(
return Err(ApplicationDownloadError::Checksum);
}
debug!(
"Successfully finished download #{}, copied {} bytes",
ctx.checksum, length
);
Ok(true)
}

View File

@ -0,0 +1,105 @@
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
};
use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
pub type DropData = v1::DropData;
static DROP_DATA_PATH: &str = ".dropdata";
pub mod v1 {
use std::{path::PathBuf, sync::Mutex};
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DropData {
pub game_id: String,
pub game_version: String,
pub contexts: Mutex<Vec<(String, bool)>>,
pub base_path: PathBuf,
}
impl DropData {
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
Self {
base_path,
game_id,
game_version,
contexts: Mutex::new(Vec::new()),
}
}
}
}
impl DropData {
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(_) => {
debug!("Generating new dropdata for game {game_id}");
return DropData::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 DropData::new(game_id, game_version, base_path);
}
};
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
Ok(manifest) => manifest,
Err(e) => {
warn!("{e}");
DropData::new(game_id, game_version, base_path)
}
}
}
pub fn write(&self) {
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
Ok(data) => data,
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_contexts(&self, completed_contexts: &[(String, bool)]) {
*self.contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec<String> {
self.contexts
.lock()
.unwrap()
.iter()
.filter_map(|x| if x.1 { Some(x.0.clone()) } else { None })
.collect()
}
pub fn get_contexts(&self) -> Vec<(String, bool)> {
info!(
"Any contexts which are complete? {}",
self.contexts.lock().unwrap().iter().any(|x| x.1)
);
self.contexts.lock().unwrap().clone()
}
}

View File

@ -1,5 +1,6 @@
pub mod commands;
pub mod download_agent;
mod download_logic;
mod drop_data;
mod manifest;
mod stored_manifest;
pub mod validate;

View File

@ -1,79 +0,0 @@
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
sync::Mutex,
};
use log::{error, warn};
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) => {
warn!("{}", 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: &[usize]) {
*self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec<usize> {
self.completed_contexts.lock().unwrap().clone()
}
}

View File

@ -0,0 +1,175 @@
use std::{
fs::File,
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
sync::{mpsc::Sender, Arc},
};
use log::{debug, error, info};
use md5::Context;
use rayon::ThreadPoolBuilder;
use crate::{
database::db::borrow_db_checked,
download_manager::{
download_manager_frontend::DownloadManagerSignal,
util::{
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
progress_object::{ProgressHandle, ProgressObject},
},
},
error::application_download_error::ApplicationDownloadError,
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
};
pub fn game_validate_logic(
dropdata: &DropData,
contexts: Vec<DropDownloadContext>,
progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
control_flag: &DownloadThreadControl,
) -> Result<bool, ApplicationDownloadError> {
progress.reset(contexts.len());
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"validating game: {} with {} threads",
dropdata.game_id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
.unwrap();
debug!("{contexts:#?}");
let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| {
for (index, context) in contexts.iter().enumerate() {
let current_progress = progress.get(index);
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
let invalid_chunks_scoped = invalid_chunks.clone();
let sender = sender.clone();
scope.spawn(move |_| {
match validate_game_chunk(context, control_flag, progress_handle) {
Ok(true) => {
debug!(
"Finished context #{} with checksum {}",
index, context.checksum
);
}
Ok(false) => {
debug!(
"Didn't finish context #{} with checksum {}",
index, &context.checksum
);
invalid_chunks_scoped.push(context.checksum.clone());
}
Err(e) => {
error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
});
}
});
// If there are any contexts left which are false
if !invalid_chunks.is_empty() {
info!(
"validation of game id {} failed for chunks {:?}",
dropdata.game_id.clone(),
invalid_chunks
);
return Ok(false);
}
Ok(true)
}
pub fn validate_game_chunk(
ctx: &DropDownloadContext,
control_flag: &DownloadThreadControl,
progress: ProgressHandle,
) -> Result<bool, ApplicationDownloadError> {
debug!(
"Starting chunk validation {}, {}, {} #{}",
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
);
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0);
return Ok(false);
}
let mut source = File::open(&ctx.path).unwrap();
if ctx.offset != 0 {
source
.seek(SeekFrom::Start(ctx.offset))
.expect("Failed to seek to file offset");
}
let mut hasher = md5::Context::new();
let completed =
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap();
if !completed {
return Ok(false);
};
let res = hex::encode(hasher.compute().0);
if res != ctx.checksum {
println!(
"Checksum failed. Correct: {}, actual: {}",
&ctx.checksum, &res
);
return Ok(false);
}
debug!(
"Successfully finished verification #{}, copied {} bytes",
ctx.checksum, ctx.length
);
Ok(true)
}
fn validate_copy(
source: &mut File,
dest: &mut Context,
size: usize,
control_flag: &DownloadThreadControl,
progress: ProgressHandle,
) -> 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, dest);
let mut total_bytes = 0;
loop {
if control_flag.get() == DownloadThreadControlFlag::Stop {
buf_writer.flush()?;
return Ok(false);
}
let mut bytes_read = source.read(&mut copy_buf)?;
total_bytes += bytes_read;
// If we read over (likely), truncate our read to
// the right size
if total_bytes > size {
let over = total_bytes - size;
bytes_read -= over;
total_bytes = size;
}
buf_writer.write_all(&copy_buf[0..bytes_read])?;
progress.add(bytes_read);
if total_bytes >= size {
break;
}
}
buf_writer.flush()?;
Ok(true)
}

View File

@ -4,30 +4,31 @@ use std::thread::spawn;
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use tauri::Emitter;
use tauri::AppHandle;
use tauri::Emitter;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
};
use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::download_manager_frontend::DownloadStatus;
use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::make_request;
use crate::{AppState, DB};
use crate::AppState;
use bitcode::{Encode, Decode};
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct {
game: Game,
status: GameStatusWithTransient,
version: Option<GameVersion>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct Game {
id: String,
@ -85,7 +86,7 @@ pub fn fetch_library_logic(
if response.status() != 200 {
let err = response.json().unwrap();
warn!("{:?}", err);
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
@ -106,8 +107,8 @@ pub fn fetch_library_logic(
}
// Add games that are installed but no longer in library
for (_, meta) in &db_handle.applications.installed_game_version {
if games.iter().find(|e| e.id == meta.id).is_some() {
for meta in db_handle.applications.installed_game_version.values() {
if games.iter().any(|e| e.id == meta.id) {
continue;
}
// We should always have a cache of the object
@ -144,7 +145,7 @@ pub fn fetch_game_logic(
) -> Result<FetchGameStruct, RemoteAccessError> {
let mut state_handle = state.lock().unwrap();
let handle = DB.borrow_data().unwrap();
let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
@ -187,7 +188,7 @@ pub fn fetch_game_logic(
}
if response.status() != 200 {
let err = response.json().unwrap();
warn!("{:?}", err);
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
@ -220,7 +221,7 @@ pub fn fetch_game_logic_offline(
id: String,
_state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let handle = DB.borrow_data().unwrap();
let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
@ -263,7 +264,7 @@ pub fn fetch_game_verion_options_logic(
if response.status() != 200 {
let err = response.json().unwrap();
warn!("{:?}", err);
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
@ -313,6 +314,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None,
} {
db_handle
@ -326,7 +331,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
let app_handle = app_handle.clone();
spawn(move || match remove_dir_all(install_dir) {
Err(e) => {
error!("{}", e);
error!("{e}");
}
Ok(_) => {
let mut db_handle = borrow_db_mut_checked();
@ -341,10 +346,9 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
save_db();
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", {}).unwrap();
app_handle.emit("update_library", ()).unwrap();
push_game_update(
&app_handle,
@ -354,6 +358,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
);
}
});
} else {
warn!("invalid previous state for uninstall, failing silently.")
}
}
@ -365,6 +371,66 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
.cloned()
}
pub fn on_game_incomplete(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", generate_authorization_header()),
)?
.send()?;
let game_version: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
.entry(meta.id.clone())
.or_default()
.insert(meta.version.clone().unwrap(), game_version.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
let status = GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.clone().unwrap(),
install_dir,
};
handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(handle);
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
},
)
.unwrap();
Ok(())
}
pub fn on_game_complete(
meta: &DownloadableMetadata,
install_dir: String,
@ -404,7 +470,6 @@ pub fn on_game_complete(
.insert(meta.id.clone(), meta.clone());
drop(handle);
save_db();
let status = if game_version.setup_command.is_empty() {
GameDownloadStatus::Installed {
@ -424,7 +489,6 @@ pub fn on_game_complete(
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(db_handle);
save_db();
app_handle
.emit(
&format!("update_game/{}", meta.id),
@ -447,7 +511,7 @@ pub fn push_game_update(
) {
app_handle
.emit(
&format!("update_game/{}", game_id),
&format!("update_game/{game_id}"),
GameUpdateEvent {
game_id: game_id.clone(),
status,
@ -468,7 +532,7 @@ pub fn update_game_configuration(
game_id: String,
options: FrontendGameOptions,
) -> Result<(), LibraryError> {
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
let installed_version = handle
.applications
.installed_game_version
@ -499,8 +563,5 @@ pub fn update_game_configuration(
.unwrap()
.insert(version.to_string(), existing_configuration);
drop(handle);
save_db();
Ok(())
}

View File

@ -1,3 +1,7 @@
#![feature(fn_traits)]
#![feature(duration_constructors)]
#![deny(clippy::all)]
mod database;
mod games;
@ -7,12 +11,13 @@ mod error;
mod process;
mod remote;
use crate::database::db::DatabaseImpls;
use crate::process::commands::open_process_logs;
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use client::commands::fetch_state;
use client::{
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
cleanup::{cleanup_and_exit, quit},
};
use client::commands::fetch_state;
use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
fetch_system_data, update_settings,
@ -22,8 +27,8 @@ use database::models::data::GameDownloadStatus;
use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
};
use download_manager::download_manager::DownloadManager;
use download_manager::download_manager_builder::DownloadManagerBuilder;
use download_manager::download_manager_frontend::DownloadManager;
use games::collections::commands::{
add_game_to_collection, create_collection, delete_collection, delete_game_in_collection,
fetch_collection, fetch_collections,
@ -49,19 +54,24 @@ use remote::commands::{
use remote::fetch_object::{fetch_object, fetch_object_offline};
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;
use std::io::Write;
use std::panic::PanicHookInfo;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
use std::{env, panic};
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_dialog::DialogExt;
use bitcode::{Encode, Decode};
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
pub enum AppStatus {
@ -74,7 +84,7 @@ pub enum AppStatus {
ServerUnavailable,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
@ -103,7 +113,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
"{d} | {l} | {f}:{L} - {m}{n}",
)))
.append(false)
.build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log"))
.build(DATA_ROOT_DIR.join("./drop.log"))
.unwrap();
let console = ConsoleAppender::builder()
@ -156,6 +166,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
for (game_id, status) in statuses.into_iter() {
match status {
GameDownloadStatus::Remote {} => {}
GameDownloadStatus::PartiallyInstalled { .. } => {}
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir,
@ -177,7 +188,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
}
}
info!("detected games missing: {:?}", missing_games);
info!("detected games missing: {missing_games:?}");
let mut db_handle = borrow_db_mut_checked();
for game_id in missing_games {
@ -194,7 +205,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
// Sync autostart state
if let Err(e) = sync_autostart_on_startup(&handle) {
warn!("failed to sync autostart state: {}", e);
warn!("failed to sync autostart state: {e}");
}
AppState {
@ -208,9 +219,31 @@ fn setup(handle: AppHandle) -> AppState<'static> {
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
let crash_file = DATA_ROOT_DIR.join(format!(
"crash-{}.log",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()?
.as_secs()
));
let mut file = File::create_new(crash_file).ok()?;
file.write_all(format!("Drop crashed with the following panic:\n{e}").as_bytes())
.ok()?;
drop(file);
Some(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
panic::set_hook(Box::new(|e| {
let _ = custom_panic_handler(e);
println!("{e}");
}));
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init());
@ -259,6 +292,7 @@ pub fn run() {
delete_game_in_collection,
// Downloads
download_game,
resume_download,
move_download_in_queue,
pause_downloads,
resume_downloads,
@ -269,6 +303,7 @@ pub fn run() {
kill_game,
toggle_autostart,
get_autostart_enabled,
open_process_logs
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
@ -300,7 +335,7 @@ pub fn run() {
.inner_size(1536.0, 864.0)
.decorations(false)
.shadow(false)
.data_directory(DATA_ROOT_DIR.lock().unwrap().join(".webview"))
.data_directory(DATA_ROOT_DIR.join(".webview"))
.build()
.unwrap();
@ -352,11 +387,7 @@ pub fn run() {
if let Some(original) = db_handle.prev_database.take() {
warn!(
"Database corrupted. Original file at {}",
original
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string()
original.canonicalize().unwrap().to_string_lossy()
);
app.dialog()
.message(
@ -412,7 +443,7 @@ pub fn run() {
});
}
fn run_on_tray<T: FnOnce() -> ()>(f: T) {
fn run_on_tray<T: FnOnce()>(f: T) {
if match std::env::var("NO_TRAY_ICON") {
Ok(s) => s.to_lowercase() != "true",
Err(_) => true,

View File

@ -38,3 +38,13 @@ pub fn kill_game(
.kill_game(game_id)
.map_err(ProcessError::IOError)
}
#[tauri::command]
pub fn open_process_logs(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock.open_process_logs(game_id)
}

View File

@ -1,12 +1,13 @@
use std::{
collections::HashMap,
fs::OpenOptions,
fs::{OpenOptions, create_dir_all},
io::{self},
path::PathBuf,
process::{Command, ExitStatus},
str::FromStr,
sync::{Arc, Mutex},
thread::spawn,
time::{Duration, SystemTime},
};
use dynfmt::Format;
@ -14,11 +15,13 @@ use dynfmt::SimpleCurlyFormat;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use shared_child::SharedChild;
use tauri::{AppHandle, Manager};
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_opener::OpenerExt;
use crate::{
AppState, DB,
database::{
db::{borrow_db_mut_checked, DATA_ROOT_DIR},
db::{DATA_ROOT_DIR, borrow_db_mut_checked},
models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
GameVersion,
@ -26,22 +29,25 @@ use crate::{
},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
AppState, DB,
};
pub struct RunningProcess {
handle: Arc<SharedChild>,
start: SystemTime,
manually_killed: bool,
}
pub struct ProcessManager<'a> {
current_platform: Platform,
log_output_dir: PathBuf,
processes: HashMap<String, Arc<SharedChild>>,
processes: HashMap<String, RunningProcess>,
app_handle: AppHandle,
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
}
impl ProcessManager<'_> {
pub fn new(app_handle: AppHandle) -> Self {
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
let log_output_dir = root_dir_lock.join("logs");
drop(root_dir_lock);
let log_output_dir = DATA_ROOT_DIR.join("logs");
ProcessManager {
#[cfg(target_os = "windows")]
@ -79,10 +85,11 @@ impl ProcessManager<'_> {
}
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
match self.processes.get(&game_id) {
Some(child) => {
child.kill()?;
child.wait()?;
match self.processes.get_mut(&game_id) {
Some(process) => {
process.manually_killed = true;
process.handle.kill()?;
process.handle.wait()?;
Ok(())
}
None => Err(io::Error::new(
@ -92,15 +99,26 @@ impl ProcessManager<'_> {
}
}
pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> {
let dir = self.log_output_dir.join(game_id);
self.app_handle
.opener()
.open_path(dir.to_str().unwrap(), None::<&str>)
.map_err(ProcessError::OpenerError)?;
Ok(())
}
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
if !self.processes.contains_key(&game_id) {
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
warn!(
"process on_finish was called, but game_id is no longer valid. finished with result: {result:?}"
);
return;
}
debug!("process for {:?} exited with {:?}", &game_id, result);
self.processes.remove(&game_id);
let process = self.processes.remove(&game_id).unwrap();
let mut db_handle = borrow_db_mut_checked();
let meta = db_handle
@ -112,39 +130,41 @@ impl ProcessManager<'_> {
db_handle.applications.transient_statuses.remove(&meta);
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
if let Some(saved_state) = current_state {
if let GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} = saved_state
{
if let Ok(exit_code) = result {
if exit_code.success() {
db_handle.applications.game_statuses.insert(
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
},
);
}
}
}
if let Some(GameDownloadStatus::SetupRequired {
version_name,
install_dir,
}) = current_state
&& let Ok(exit_code) = result
&& exit_code.success()
{
db_handle.applications.game_statuses.insert(
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
},
);
}
drop(db_handle);
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
// If we started and ended really quickly, something might've gone wrong
// Or if the status isn't 0
// Or if it's an error
if !process.manually_killed
&& (elapsed.as_secs() <= 2 || result.is_err() || !result.unwrap().success())
{
warn!("drop detected that the game {game_id} may have failed to launch properly");
let _ = self.app_handle.emit("launch_external_error", &game_id);
}
let status = GameStatusManager::fetch_state(&game_id);
push_game_update(&self.app_handle, &game_id, None, status);
// TODO better management
}
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
let current = &self.current_platform;
Ok(self
.game_launchers
.contains_key(&(current.clone(), platform.clone())))
Ok(self.game_launchers.contains_key(&(*current, *platform)))
}
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
@ -162,7 +182,7 @@ impl ProcessManager<'_> {
{
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
Some(GameDownloadStatus::SetupRequired { .. }) => {
return Err(ProcessError::SetupRequired)
return Err(ProcessError::SetupRequired);
}
_ => return Err(ProcessError::NotInstalled),
};
@ -193,6 +213,10 @@ impl ProcessManager<'_> {
version_name,
install_dir,
} => (version_name, install_dir),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded),
};
@ -204,18 +228,17 @@ impl ProcessManager<'_> {
.get(version_name)
.ok_or(ProcessError::InvalidVersion)?;
// TODO: refactor this path with open_process_logs
let game_log_folder = &self.log_output_dir.join(game_id);
create_dir_all(game_log_folder).map_err(ProcessError::IOError)?;
let current_time = chrono::offset::Local::now();
let log_file = OpenOptions::new()
.write(true)
.truncate(true)
.read(true)
.create(true)
.open(self.log_output_dir.join(format!(
"{}-{}-{}.log",
&game_id,
&version,
current_time.timestamp()
)))
.open(game_log_folder.join(format!("{}-{}.log", &version, current_time.timestamp())))
.map_err(ProcessError::IOError)?;
let error_file = OpenOptions::new()
@ -223,16 +246,15 @@ impl ProcessManager<'_> {
.truncate(true)
.read(true)
.create(true)
.open(self.log_output_dir.join(format!(
"{}-{}-{}-error.log",
&game_id,
.open(game_log_folder.join(format!(
"{}-{}-error.log",
&version,
current_time.timestamp()
)))
.map_err(ProcessError::IOError)?;
let current_platform = self.current_platform.clone();
let target_platform = game_version.platform.clone();
let current_platform = self.current_platform;
let target_platform = game_version.platform;
let game_launcher = self
.game_launchers
@ -248,10 +270,14 @@ impl ProcessManager<'_> {
version_name: _,
install_dir: _,
} => (&game_version.setup_command, &game_version.setup_args),
GameDownloadStatus::Remote {} => unreachable!("nuh uh"),
GameDownloadStatus::PartiallyInstalled {
version_name: _,
install_dir: _,
} => unreachable!("Game registered as 'Partially Installed'"),
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
};
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
let launch = launch.to_str().unwrap();
let launch_string = game_launcher.create_launch_process(
@ -275,12 +301,12 @@ impl ProcessManager<'_> {
#[cfg(target_os = "windows")]
command.raw_arg(format!("/C \"{}\"", &launch_string));
info!("launching (in {}): {}", install_dir, launch_string,);
info!("launching (in {install_dir}): {launch_string}",);
#[cfg(unix)]
let mut command: Command = Command::new("sh");
#[cfg(unix)]
command.arg("-c").arg(launch_string);
command.args(vec!["-c", &launch_string]);
command
.stderr(error_file)
@ -323,7 +349,14 @@ impl ProcessManager<'_> {
drop(app_state_handle);
});
self.processes.insert(meta.id, wait_thread_handle);
self.processes.insert(
meta.id,
RunningProcess {
handle: wait_thread_handle,
start: SystemTime::now(),
manually_killed: false,
},
);
Ok(())
}
}
@ -336,9 +369,6 @@ pub enum Platform {
}
impl Platform {
const WINDOWS: bool = cfg!(target_os = "windows");
const MAC: bool = cfg!(target_os = "macos");
const LINUX: bool = cfg!(target_os = "linux");
#[cfg(target_os = "windows")]
pub const HOST: Platform = Self::Windows;
#[cfg(target_os = "macos")]
@ -346,8 +376,6 @@ impl Platform {
#[cfg(target_os = "linux")]
pub const HOST: Platform = Self::Linux;
pub fn is_case_sensitive(&self) -> bool {
match self {
Self::Windows | Self::MacOs => false,
@ -373,7 +401,7 @@ impl From<whoami::Platform> for Platform {
whoami::Platform::Windows => Platform::Windows,
whoami::Platform::Linux => Platform::Linux,
whoami::Platform::MacOS => Platform::MacOs,
_ => unimplemented!()
_ => unimplemented!(),
}
}
}
@ -416,10 +444,13 @@ impl ProcessHandler for UMULauncher {
) -> String {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => game_override
.is_empty()
.then_some(game_version.game_id.clone())
.unwrap_or(game_override.clone()),
Some(game_override) => {
if game_override.is_empty() {
game_version.game_id.clone()
} else {
game_override.clone()
}
}
None => game_version.game_id.clone(),
};
format!(

View File

@ -1,20 +1,20 @@
use std::{collections::HashMap, env};
use std::{collections::HashMap, env, sync::Mutex};
use chrono::Utc;
use droplet_rs::ssl::sign_nonce;
use gethostname::gethostname;
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::{
db::{borrow_db_checked, borrow_db_mut_checked, save_db},
db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth,
},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppStatus, User,
AppState, AppStatus, User,
};
use super::{
@ -72,7 +72,7 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
.send()?;
if response.status() != 200 {
let err: DropServerError = response.json()?;
warn!("{:?}", err);
warn!("{err:?}");
if err.status_message == "Nonce expired" {
return Err(RemoteAccessError::OutOfSync);
@ -122,8 +122,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
client_id: response_struct.id,
web_token: None, // gets created later
});
drop(handle);
save_db();
}
let web_token = {
@ -140,8 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let mut handle = borrow_db_mut_checked();
let mut_auth = handle.auth.as_mut().unwrap();
mut_auth.web_token = Some(web_token);
drop(handle);
save_db();
Ok(())
}
@ -152,11 +148,21 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
let handshake_result = recieve_handshake_logic(&app, path);
if let Err(e) = handshake_result {
warn!("error with authentication: {}", e);
warn!("error with authentication: {e}");
app.emit("auth/failed", e.to_string()).unwrap();
return;
}
let app_state = app.state::<Mutex<AppState>>();
let mut state_lock = app_state.lock().unwrap();
let (app_status, user) = setup();
state_lock.status = app_status;
state_lock.user = user;
drop(state_lock);
app.emit("auth/finished", ()).unwrap();
}
@ -206,7 +212,7 @@ pub fn setup() -> (AppStatus, Option<User>) {
let user_result = match fetch_user() {
Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => {
let user = get_cached_object::<String, User>("user".to_owned()).unwrap();
let user = get_cached_object::<_, User>("user").unwrap();
return (AppStatus::Offline, Some(user));
}
Err(_) => return (AppStatus::SignedInNeedsReauth, None),

View File

@ -1,17 +1,21 @@
use std::{
fmt::Display,
time::{Duration, SystemTime},
};
use crate::{
database::{db::borrow_db_checked, models::data::Database},
error::remote_access_error::RemoteAccessError,
};
use bitcode::{Decode, DecodeOwned, Encode};
use cacache::Integrity;
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
#[macro_export]
macro_rules! offline {
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
if crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == crate::AppStatus::Offline {
if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
$func2( $( $arg ), *)
} else {
$func1( $( $arg ), *)
@ -19,31 +23,48 @@ macro_rules! offline {
}
}
pub fn cache_object<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
pub fn cache_object<K: AsRef<str>, D: Encode>(
key: K,
data: &D,
) -> Result<Integrity, RemoteAccessError> {
let bytes = serde_binary::to_vec(data, Endian::Little).unwrap();
let bytes = bitcode::encode(data);
cacache::write_sync(&borrow_db_checked().cache_dir, key, bytes)
.map_err(|e| RemoteAccessError::Cache(e))
.map_err(RemoteAccessError::Cache)
}
pub fn get_cached_object<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
pub fn get_cached_object<K: AsRef<str> + Display, D: Encode + DecodeOwned>(
key: K,
) -> Result<D, RemoteAccessError> {
get_cached_object_db::<K, D>(key, &borrow_db_checked())
}
pub fn get_cached_object_db<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
pub fn get_cached_object_db<K: AsRef<str> + Display, D: DecodeOwned>(
key: K,
db: &Database,
) -> Result<D, RemoteAccessError> {
let bytes = cacache::read_sync(&db.cache_dir, key).map_err(|e| RemoteAccessError::Cache(e))?;
let data = serde_binary::from_slice::<D>(&bytes, Endian::Little).unwrap();
let bytes = cacache::read_sync(&db.cache_dir, &key).map_err(RemoteAccessError::Cache)?;
let data = bitcode::decode::<D>(&bytes).map_err(|_| {
RemoteAccessError::Cache(cacache::Error::EntryNotFound(
db.cache_dir.clone(),
key.to_string(),
))
})?;
Ok(data)
}
#[derive(Serialize, Deserialize)]
#[derive(Encode, Decode)]
pub struct ObjectCache {
content_type: String,
body: Vec<u8>,
expiry: u128,
}
impl ObjectCache {
pub fn has_expired(&self) -> bool {
let duration = Duration::from_millis(self.expiry.try_into().unwrap());
SystemTime::UNIX_EPOCH
.checked_add(duration)
.unwrap()
.elapsed()
.is_err()
}
}
impl From<Response<Vec<u8>>> for ObjectCache {
@ -57,6 +78,12 @@ impl From<Response<Vec<u8>>> for ObjectCache {
.unwrap()
.to_owned(),
body: value.body().clone(),
expiry: SystemTime::now()
.checked_add(Duration::from_days(1))
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis(),
}
}
}
@ -66,3 +93,9 @@ impl From<ObjectCache> for Response<Vec<u8>> {
resp_builder.body(value.body).unwrap()
}
}
impl From<&ObjectCache> for Response<Vec<u8>> {
fn from(value: &ObjectCache) -> Self {
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
resp_builder.body(value.body.clone()).unwrap()
}
}

View File

@ -6,7 +6,7 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
database::db::{borrow_db_checked, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request},
AppState, AppStatus,
@ -15,7 +15,7 @@ use crate::{
use super::{
auth::{auth_initiate_logic, recieve_handshake, setup},
cache::{cache_object, get_cached_object},
remote::use_remote_logic,
utils::use_remote_logic,
};
#[tauri::command]
@ -54,7 +54,7 @@ pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
Ok(data)
}
Err(e) => {
debug!("{}", e);
debug!("{e}");
get_cached_object::<&str, Vec<u8>>(&path)
}
}
@ -65,8 +65,6 @@ pub fn sign_out(app: AppHandle) {
{
let mut handle = borrow_db_mut_checked();
handle.auth = None;
drop(handle);
save_db();
}
// Update app state
@ -98,5 +96,5 @@ pub fn auth_initiate() -> Result<(), RemoteAccessError> {
#[tauri::command]
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
recieve_handshake(app, format!("handshake/{}", token));
recieve_handshake(app, format!("handshake/{token}"));
}

View File

@ -12,6 +12,14 @@ pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeRespond
// Drop leading /
let object_id = &request.uri().path()[1..];
let cache_result = get_cached_object::<&str, ObjectCache>(object_id);
if let Ok(cache_result) = &cache_result
&& !cache_result.has_expired()
{
responder.respond(cache_result.into());
return;
}
let header = generate_authorization_header();
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
@ -20,12 +28,10 @@ pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeRespond
.unwrap()
.send();
if response.is_err() {
let data = get_cached_object::<&str, ObjectCache>(object_id);
match data {
Ok(data) => responder.respond(data.into()),
match cache_result {
Ok(cache_result) => responder.respond(cache_result.into()),
Err(e) => {
warn!("{}", e)
warn!("{e}")
}
}
return;
@ -38,7 +44,9 @@ pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeRespond
);
let data = Vec::from(response.bytes().unwrap());
let resp = resp_builder.body(data).unwrap();
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
if cache_result.is_err() || cache_result.unwrap().has_expired() {
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
}
responder.respond(resp);
}
@ -48,6 +56,6 @@ pub fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchem
match data {
Ok(data) => responder.respond(data.into()),
Err(e) => warn!("{}", e),
Err(e) => warn!("{e}"),
}
}

View File

@ -3,6 +3,6 @@ pub mod auth;
pub mod cache;
pub mod commands;
pub mod fetch_object;
pub mod remote;
pub mod requests;
pub mod server_proto;
pub mod utils;

View File

@ -1,9 +1,6 @@
use std::str::FromStr;
use http::{
uri::PathAndQuery,
Request, Response, StatusCode, Uri,
};
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
use reqwest::blocking::Client;
use tauri::UriSchemeResponder;
@ -29,18 +26,14 @@ pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeRespon
let mut new_uri = request.uri().clone().into_parts();
new_uri.path_and_query =
Some(PathAndQuery::from_str(&format!("{}?noWrapper=true", path)).unwrap());
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).unwrap());
new_uri.authority = remote_uri.authority().cloned();
new_uri.scheme = remote_uri.scheme().cloned();
let new_uri = Uri::from_parts(new_uri).unwrap();
let whitelist_prefix = vec!["/store", "/api", "/_", "/fonts"];
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
if whitelist_prefix
.iter()
.map(|f| !path.starts_with(f))
.all(|f| f)
{
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
webbrowser::open(&new_uri.to_string()).unwrap();
return;
}
@ -48,7 +41,7 @@ pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeRespon
let client = Client::new();
let response = client
.request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {}", web_token))
.header("Authorization", format!("Bearer {web_token}"))
.headers(request.headers().clone())
.send()
.unwrap();

View File

@ -5,9 +5,8 @@ use serde::Deserialize;
use url::Url;
use crate::{
database::db::{borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError,
AppState, AppStatus,
database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppState,
AppStatus,
};
#[derive(Deserialize)]
@ -20,7 +19,7 @@ pub fn use_remote_logic(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<(), RemoteAccessError> {
debug!("connecting to url {}", url);
debug!("connecting to url {url}");
let base_url = Url::parse(&url)?;
// Test Drop url
@ -40,9 +39,6 @@ pub fn use_remote_logic(
let mut db_state = borrow_db_mut_checked();
db_state.base_url = base_url.to_string();
drop(db_state);
save_db();
Ok(())
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.3.0-rc-2",
"version": "0.3.0-rc-8",
"identifier": "dev.drop.app",
"build": {
"beforeDevCommand": "yarn dev --port 1432",
@ -11,7 +11,11 @@
},
"app": {
"security": {
"csp": null
"csp": null,
"assetProtocol": {
"enable": true,
"scope": {}
}
}
},
"plugins": {

View File

@ -59,11 +59,13 @@ export enum GameStatusEnum {
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
Running = "Running",
PartiallyInstalled = "PartiallyInstalled"
}
export type GameStatus = {
type: GameStatusEnum;
version_name?: string;
install_dir?: string;
};
export enum DownloadableType {

View File

@ -1508,6 +1508,11 @@
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.1.tgz#dc49d899fb873b96ee1d46a171384625ba5ad404"
integrity sha512-eoQWT+Tq1qSwQpHV+nw1eNYe5B/nm1PoRjQCRiEOS12I1b+X4PUcREfXVX8dPcBT6GrzWGDtaecY0+1p0Rfqlw==
"@tauri-apps/api@^2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.6.0.tgz#efd873bf04b0d72cea81f9397e16218f5deafe0f"
integrity sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==
"@tauri-apps/cli-darwin-arm64@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.1.tgz#5816c0099977f705d1a7249822fa51f5d3c3750a"
@ -1588,6 +1593,13 @@
dependencies:
"@tauri-apps/api" "^2.0.0"
"@tauri-apps/plugin-opener@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-opener/-/plugin-opener-2.4.0.tgz#57eae5998e1c396791af16832a9dde16eca06439"
integrity sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==
dependencies:
"@tauri-apps/api" "^2.6.0"
"@tauri-apps/plugin-os@~2":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz#ef5511269f59c0ccc580a9d09600034cfaa9743b"