Compare commits

...

11 Commits

Author SHA1 Message Date
f733fbba65 chore: Make the rest of clippy happy
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-24 14:39:22 +10:00
0d78375f54 chore: Remove lint
Because not everything is actually resolved yet: will be resolved with a restructure of the library

Signed-off-by: quexeky <git@quexeky.dev>
2025-09-24 14:37:00 +10:00
d6a9994f8b Merge branch 'develop' into 139-add-and-resolve-clippy-lints-to-prevent-unwrap-and-expect-functions
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-24 11:33:56 +10:00
dcb7455954 chore: Clippy unwrap linting
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-24 09:58:10 +10:00
70cecdad19 Update README.md 2025-09-11 08:16:33 +10:00
463c5e6f3b chore: Remove unwraps from process_handlers
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-10 09:24:27 +10:00
83dc773b10 chore: Remove unwraps from auth.rs
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-09 16:13:16 +10:00
ddde547c08 chore: Remove unwraps from fetch_object and server_proto
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-09 09:58:59 +10:00
3f18d15d39 Collections & download stability, UI (#130)
* feat: different local path in dev #73

* feat: better error output for downloads

* feat: collections in library view

* feat: improve download manager reliability

* feat: new download UI, more stable downloads

* fix: clippy

* fix: only show admin link if user is admin

* feat: check for libs before building
2025-09-07 15:57:06 +10:00
be5500d29f chore: Add CacheError and remove unwraps from fetch_object
Signed-off-by: quexeky <git@quexeky.dev>
2025-09-05 17:57:31 +10:00
97b5cd5e78 Native model fixes (#137)
* fix: Fix native_model from requirements and  add version requirements for models

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

* fix: Use Drop-OSS/native_model

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

* chore: Bump version to include logging

(Albeit, logging occurs before we initialise the logger, but oh well)

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

* chore: Make clippy happy

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

---------

Signed-off-by: quexeky <git@quexeky.dev>
2025-09-05 15:31:28 +10:00
58 changed files with 6277 additions and 710 deletions

View File

@ -1,29 +1,21 @@
# Drop App # Drop Desktop Client
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI. The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server.
## Running ## Internals
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 Docs](https://docs.droposs.org/docs/guides/quickstart)
## Current features It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
Currently supported are the following features:
- Signin (with custom server)
- Database registering & recovery
- Dynamic library fetching from server
- Installing & uninstalling games
- Download progress monitoring
- Launching / playing games
## Development ## Development
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
Install dependencies with `yarn` Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`.
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh` Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`: To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
e.g. `RUST_LOG=debug yarn tauri dev` e.g. `RUST_LOG=debug yarn tauri dev`
## Contributing ## Contributing
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines. Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing).

View File

@ -21,6 +21,13 @@ async function spawn(exec, opts) {
}); });
} }
const expectedLibs = ["drop-base/package.json"];
for (const lib of expectedLibs) {
const path = `./libs/${lib}`;
if (!fs.existsSync(path)) throw `Missing "${expectedLibs}". Run "git submodule update --init --recursive"`;
}
const views = fs.readdirSync(".").filter((view) => { const views = fs.readdirSync(".").filter((view) => {
const expectedPath = `./${view}/package.json`; const expectedPath = `./${view}/package.json`;
return fs.existsSync(expectedPath); return fs.existsSync(expectedPath);

View File

@ -1,5 +1,5 @@
<template> <template>
<LoadingIndicator /> <NuxtLoadingIndicator color="#2563eb" />
<NuxtLayout class="select-none w-screen h-screen"> <NuxtLayout class="select-none w-screen h-screen">
<NuxtPage /> <NuxtPage />
<ModalStack /> <ModalStack />

View File

@ -37,7 +37,7 @@
</NuxtLink> </NuxtLink>
<div class="h-0.5 rounded-full w-full bg-zinc-800" /> <div class="h-0.5 rounded-full w-full bg-zinc-800" />
<div class="flex flex-col mb-1"> <div class="flex flex-col mb-1">
<MenuItem v-slot="{ active }"> <MenuItem v-if="state.user.admin" v-slot="{ active }">
<a <a
:href="adminUrl" :href="adminUrl"
target="_blank" target="_blank"

View File

@ -1,55 +1,118 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="mb-3 inline-flex gap-x-2"> <div class="mb-3 inline-flex gap-x-2">
<div class="relative transition-transform duration-300 hover:scale-105 active:scale-95"> <div
<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> </div>
<input type="text" v-model="searchQuery" <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" 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..." /> placeholder="Search library..."
/>
</div> </div>
<button @click="() => calculateGames(true)" <button
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"> @click="() => calculateGames(true, 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" /> <ArrowPathIcon class="size-4" />
</button> </button>
</div> </div>
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5"> <TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[ <Disclosure
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50', as="div"
navIndex === currentNavigation v-for="(nav, navIndex) in filteredNavigation"
:key="nav.id"
class="first:pt-0 last:pb-0"
v-slot="{ open }"
:default-open="nav.deft"
>
<dt>
<DisclosureButton
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
>
<span class="text-sm font-semibold font-display">{{
nav.name
}}</span>
<span class="ml-6 flex h-7 items-center">
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
</span>
</DisclosureButton>
</dt>
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
<NuxtLink
v-for="item in nav.items"
:key="nav.id"
:class="[
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
currentNavigation == item.id
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20' ? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
: nav.isInstalled.value : item.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200' ? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300', : 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]" :href="nav.route"> ]"
<div class="flex items-center w-full gap-x-3"> :href="item.route"
<div class="flex-none transition-transform duration-300 hover:-rotate-2"> >
<img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm" <div class="flex items-center w-full gap-x-2">
:src="icons[nav.id]" alt="" /> <div
class="flex-none transition-transform duration-300 hover:-rotate-2"
>
<img
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
:src="icons[item.id]"
alt=""
/>
</div> </div>
<div class="flex flex-col flex-1"> <div class="inline-flex items-center gap-x-2">
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold"> <p
{{ nav.label }} class="text-sm whitespace-nowrap font-display font-semibold"
>
{{ item.label }}
</p> </p>
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]"> <p
{{ gameStatusText[games[nav.id].status.value.type] }} class="truncate text-[10px] font-bold uppercase font-display"
:class="[
gameStatusTextStyle[games[item.id].status.value.type],
]"
>
{{ gameStatusText[games[item.id].status.value.type] }}
</p> </p>
</div> </div>
</div> </div>
</NuxtLink> </NuxtLink>
</DisclosurePanel>
</Disclosure>
</TransitionGroup> </TransitionGroup>
<div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100"> <div
v-if="loading"
class="h-full grow flex p-8 justify-center text-zinc-100"
>
<div role="status"> <div role="status">
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101" <svg
fill="none" xmlns="http://www.w3.org/2000/svg"> aria-hidden="true"
class="w-6 h-6 text-transparent animate-spin fill-zinc-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" /> fill="currentColor"
/>
<path <path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" /> fill="currentFill"
/>
</svg> </svg>
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
@ -58,9 +121,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid"; import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
import {
ArrowPathIcon,
MagnifyingGlassIcon,
MinusSmallIcon,
PlusSmallIcon,
} from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type GameStatus } from "~/types"; import {
GameStatusEnum,
type Collection as Collection,
type Game,
type GameStatus,
} from "~/types";
import { TransitionGroup } from "vue"; import { TransitionGroup } from "vue";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
@ -70,7 +144,7 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Downloading]: "text-zinc-400", [GameStatusEnum.Downloading]: "text-zinc-400",
[GameStatusEnum.Validating]: "text-blue-300", [GameStatusEnum.Validating]: "text-blue-300",
[GameStatusEnum.Running]: "text-green-500", [GameStatusEnum.Running]: "text-green-500",
[GameStatusEnum.Remote]: "text-zinc-500", [GameStatusEnum.Remote]: "text-zinc-700",
[GameStatusEnum.Queued]: "text-zinc-400", [GameStatusEnum.Queued]: "text-zinc-400",
[GameStatusEnum.Updating]: "text-zinc-400", [GameStatusEnum.Updating]: "text-zinc-400",
[GameStatusEnum.Uninstalling]: "text-zinc-100", [GameStatusEnum.Uninstalling]: "text-zinc-100",
@ -100,26 +174,47 @@ const games: {
} = {}; } = {};
const icons: { [key: string]: string } = {}; const icons: { [key: string]: string } = {};
const rawGames: Ref<Game[], Game[]> = ref([]); const collections: Ref<Collection[]> = ref([]);
async function calculateGames(clearAll = false) { async function calculateGames(clearAll = false, forceRefresh = false) {
if (clearAll) { if (clearAll) {
rawGames.value = []; collections.value = [];
loading.value = true; loading.value = true;
} }
// If we update immediately, the navigation gets re-rendered before we // If we update immediately, the navigation gets re-rendered before we
// add all the necessary state, and it freaks tf out // add all the necessary state, and it freaks tf out
const newGames = await invoke<typeof rawGames.value>("fetch_library"); const newGames = await invoke<Game[]>("fetch_library", {
for (const game of newGames) { hardRefresh: forceRefresh,
});
const otherCollections = await invoke<Collection[]>("fetch_collections", {
hardRefresh: forceRefresh,
});
const allGames = [
...newGames,
...otherCollections
.map((e) => e.entries)
.flat()
.map((e) => e.game),
].filter((v, i, a) => a.indexOf(v) === i);
for (const game of allGames) {
if (games[game.id]) continue; if (games[game.id]) continue;
games[game.id] = await useGame(game.id); games[game.id] = await useGame(game.id);
} }
for (const game of newGames) { for (const game of allGames) {
if (icons[game.id]) continue; if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconObjectId); icons[game.id] = await useObject(game.mIconObjectId);
} }
const libraryCollection = {
id: "library",
name: "Library",
isDefault: true,
entries: newGames.map((e) => ({ gameId: e.id, game: e })),
} satisfies Collection;
loading.value = false; loading.value = false;
rawGames.value = newGames; collections.value = [libraryCollection, ...otherCollections];
} }
// Wait up to 300 ms for the library to load, otherwise // Wait up to 300 ms for the library to load, otherwise
@ -128,20 +223,19 @@ await new Promise<void>((r) => {
let hasResolved = false; let hasResolved = false;
const resolveFunc = () => { const resolveFunc = () => {
if (!hasResolved) r(); if (!hasResolved) r();
hasResolved = true hasResolved = true;
};
}
calculateGames(true).then(resolveFunc); calculateGames(true).then(resolveFunc);
setTimeout(resolveFunc, 300); setTimeout(resolveFunc, 300);
}) });
const navigation = computed(() => const navigation = computed(() =>
rawGames.value.map((game) => { collections.value.map((collection) => {
const items = collection.entries.map(({ game }) => {
const status = games[game.id].status; const status = games[game.id].status;
const isInstalled = computed( const isInstalled = computed(
() => () => status.value.type != GameStatusEnum.Remote
status.value.type != GameStatusEnum.Remote
); );
const item = { const item = {
@ -152,12 +246,20 @@ const navigation = computed(() =>
id: game.id, id: game.id,
}; };
return item; return item;
});
return {
id: collection.id,
name: collection.name,
deft: collection.isDefault,
items,
};
}) })
); );
const route = useRoute(); const route = useRoute();
const currentNavigation = computed(() => { const currentNavigation = computed(() => {
return navigation.value.findIndex((e) => e.route == route.path) return route.path.slice("/library/".length);
}); });
const filteredNavigation = computed(() => { const filteredNavigation = computed(() => {
@ -165,15 +267,18 @@ const filteredNavigation = computed(() => {
return navigation.value.map((e, i) => ({ ...e, index: i })); return navigation.value.map((e, i) => ({ ...e, index: i }));
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
return navigation.value return navigation.value
.filter((nav) => nav.label.toLowerCase().includes(query)) .map((c) => ({
.map((e, i) => ({ ...e, index: i })); ...c,
items: c.items.filter((nav) => nav.label.toLowerCase().includes(query)),
}))
.filter((e) => e.items.length > 0);
}); });
listen("update_library", async (event) => { listen("update_library", async (event) => {
console.log("Updating library"); console.log("Updating library");
let oldNavigation = navigation.value[currentNavigation.value]; let oldNavigation = currentNavigation.value;
await calculateGames(); await calculateGames();
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) { if (oldNavigation !== currentNavigation.value) {
router.push("/library"); router.push("/library");
} }
}); });

View File

@ -1,7 +0,0 @@
<template></template>
<script setup lang="ts">
const loading = useLoadingIndicator();
watch(loading.isLoading, console.log);
</script>

View File

@ -32,3 +32,5 @@ listen("update_stats", (event) => {
const stats = useStatsState(); const stats = useStatsState();
stats.value = event.payload as StatsState; stats.value = event.payload as StatsState;
}); });
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);

View File

@ -4,18 +4,18 @@
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900" class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
> >
<div <div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2" class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
> >
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span> <span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
<span v-if="stats.time > 0" class="text-sm" <span v-if="stats.time > 0" class="text-xs text-zinc-400"
>{{ formatTime(stats.time) }} left</span >{{ formatTime(stats.time) }} left</span
> >
</div> </div>
<div class="absolute inset-0 h-full flex flex-row items-end justify-end"> <div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
<div <div
v-for="bar in speedHistory" v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }" :style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[8px] bg-blue-600/40" class="w-[3px] bg-blue-600 rounded-t-full"
/> />
</div> </div>
</div> </div>
@ -62,9 +62,9 @@
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display" class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{ ><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000) formatKilobytes(element.current / 1000)
}}</span> }}B</span>
/ /
<span class="">{{ formatKilobytes(element.max / 1000) }}</span <span class="">{{ formatKilobytes(element.max / 1000) }}B</span
><ServerIcon class="size-5" ><ServerIcon class="size-5"
/></span> /></span>
</div> </div>
@ -91,7 +91,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid"; import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types"; import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
// const actionNames = { // const actionNames = {
// [GameStatusEnum.Downloading]: "downloading", // [GameStatusEnum.Downloading]: "downloading",
@ -105,12 +105,12 @@ window.addEventListener("resize", (event) => {
const queue = useQueueState(); const queue = useQueueState();
const stats = useStatsState(); const stats = useStatsState();
const speedHistory = useState<Array<number>>(() => []); const speedHistory = useDownloadHistory();
const speedHistoryMax = computed(() => windowWidth.value / 8); const speedHistoryMax = computed(() => windowWidth.value / 4);
const speedMax = computed( const speedMax = computed(
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3 () => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
); );
const previousGameId = ref<string | undefined>(); const previousGameId = useState<string | undefined>('previous_game');
const games: Ref<{ const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string }; [key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
@ -122,14 +122,15 @@ function resetHistoryGraph() {
} }
function checkReset(v: QueueState) { function checkReset(v: QueueState) {
const currentGame = v.queue.at(0)?.meta.id; const currentGame = v.queue.at(0)?.meta.id;
// If we don't have a game
if (!currentGame) return;
// If we're finished // If we're finished
if (!currentGame && previousGameId.value) { if (!currentGame && previousGameId.value) {
previousGameId.value = undefined; previousGameId.value = undefined;
resetHistoryGraph(); resetHistoryGraph();
return; return;
} }
// If we don't have a game
if (!currentGame) return;
// If we started a new download // If we started a new download
if (currentGame && !previousGameId.value) { if (currentGame && !previousGameId.value) {
previousGameId.value = currentGame; previousGameId.value = currentGame;
@ -149,9 +150,10 @@ watch(queue, (v) => {
}); });
watch(stats, (v) => { watch(stats, (v) => {
if(v.speed == 0) return;
const newLength = speedHistory.value.push(v.speed); const newLength = speedHistory.value.push(v.speed);
if (newLength > speedHistoryMax.value) { if (newLength > speedHistoryMax.value) {
speedHistory.value.splice(0, 1); speedHistory.value.splice(0, newLength - speedHistoryMax.value);
} }
checkReset(queue.value); checkReset(queue.value);
}); });
@ -183,7 +185,7 @@ async function cancelGame(meta: DownloadableMetadata) {
} }
function formatKilobytes(bytes: number): string { function formatKilobytes(bytes: number): string {
const units = ["KB", "MB", "GB", "TB", "PB"]; const units = ["K", "M", "G", "T", "P"];
let value = bytes; let value = bytes;
let unitIndex = 0; let unitIndex = 0;
const scalar = 1000; const scalar = 1000;

View File

@ -37,6 +37,13 @@ export type Game = {
mImageCarouselObjectIds: string[]; mImageCarouselObjectIds: string[];
}; };
export type Collection = {
id: string;
name: string;
isDefault: boolean;
entries: Array<{ gameId: string; game: Game }>;
};
export type GameVersion = { export type GameVersion = {
launchCommandTemplate: string; launchCommandTemplate: string;
}; };

View File

@ -14,7 +14,8 @@
"@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-shell": "^2.3.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-pretty": "^13.1.1" "pino-pretty": "^13.1.1",
"tauri": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.7.1" "@tauri-apps/cli": "^2.7.1"

31
src-tauri/Cargo.lock generated
View File

@ -1289,6 +1289,7 @@ dependencies = [
"atomic-instant-full", "atomic-instant-full",
"bitcode", "bitcode",
"boxcar", "boxcar",
"bytes",
"cacache 13.1.0", "cacache 13.1.0",
"chrono", "chrono",
"deranged", "deranged",
@ -1296,6 +1297,7 @@ dependencies = [
"droplet-rs", "droplet-rs",
"dynfmt", "dynfmt",
"filetime", "filetime",
"futures-core",
"futures-lite", "futures-lite",
"gethostname", "gethostname",
"hex 0.4.3", "hex 0.4.3",
@ -1339,6 +1341,7 @@ dependencies = [
"tempfile", "tempfile",
"throttle_my_fn", "throttle_my_fn",
"tokio", "tokio",
"tokio-util",
"umu-wrapper-lib", "umu-wrapper-lib",
"url", "url",
"urlencoding", "urlencoding",
@ -3118,13 +3121,13 @@ dependencies = [
[[package]] [[package]]
name = "native_model" name = "native_model"
version = "0.6.1" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/Drop-OSS/native_model.git#a91b422cbd53116df1f20b2459fb3d8257458bfd"
checksum = "7050d759e3da6673361dddda4f4a743492279dd2c6484a21fbee0a8278620df0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
"doc-comment", "doc-comment",
"log",
"native_model_macro", "native_model_macro",
"rmp-serde", "rmp-serde",
"serde", "serde",
@ -3134,10 +3137,10 @@ dependencies = [
[[package]] [[package]]
name = "native_model_macro" name = "native_model_macro"
version = "0.6.1" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/Drop-OSS/native_model.git#a91b422cbd53116df1f20b2459fb3d8257458bfd"
checksum = "1577a0bebf5ed1754e240baf5d9b1845f51e598b20600aa894f55e11cd20cc6c"
dependencies = [ dependencies = [
"log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.101", "syn 2.0.101",
@ -5668,9 +5671,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.2.2" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8" checksum = "37e5858cc7b455a73ab4ea2ebc08b5be33682c00ff1bf4cad5537d4fb62499d9"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@ -5686,9 +5689,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.3.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da" checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@ -5748,9 +5751,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.2.1" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d" checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@ -6083,9 +6086,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.15" version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",

View File

@ -65,7 +65,7 @@ whoami = "1.6.0"
filetime = "0.2.25" filetime = "0.2.25"
walkdir = "2.5.0" walkdir = "2.5.0"
known-folders = "1.2.0" known-folders = "1.2.0"
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] } native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
tauri-plugin-opener = "2.4.0" tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6" bitcode = "0.6.6"
reqwest-websocket = "0.5.0" reqwest-websocket = "0.5.0"
@ -73,6 +73,9 @@ futures-lite = "2.6.0"
page_size = "0.6.0" page_size = "0.6.0"
sysinfo = "0.36.1" sysinfo = "0.36.1"
humansize = "2.1.3" humansize = "2.1.3"
tokio-util = { version = "0.7.16", features = ["io"] }
futures-core = "0.3.31"
bytes = "1.10.1"
# tailscale = { path = "./tailscale" } # tailscale = { path = "./tailscale" }
[dependencies.dynfmt] [dependencies.dynfmt]
@ -106,7 +109,15 @@ features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
[dependencies.reqwest] [dependencies.reqwest]
version = "0.12.22" version = "0.12.22"
default-features = false default-features = false
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"] features = [
"json",
"http2",
"blocking",
"rustls-tls",
"native-tls-alpn",
"rustls-tls-native-roots",
"stream",
]
[dependencies.serde] [dependencies.serde]
version = "1" version = "1"

View File

@ -1,7 +1,7 @@
use log::{debug, error}; use log::{debug, error};
use tauri::AppHandle; use tauri::AppHandle;
use crate::AppState; use crate::{lock, AppState};
#[tauri::command] #[tauri::command]
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) { pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
@ -10,7 +10,7 @@ pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppS
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) { pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
debug!("cleaning up and exiting application"); debug!("cleaning up and exiting application");
let download_manager = state.lock().unwrap().download_manager.clone(); let download_manager = lock!(state).download_manager.clone();
match download_manager.ensure_terminated() { match download_manager.ensure_terminated() {
Ok(res) => match res { Ok(res) => match res {
Ok(()) => debug!("download manager terminated correctly"), Ok(()) => debug!("download manager terminated correctly"),

View File

@ -1,10 +1,10 @@
use crate::AppState; use crate::{lock, AppState};
#[tauri::command] #[tauri::command]
pub fn fetch_state( pub fn fetch_state(
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
) -> Result<String, String> { ) -> Result<String, String> {
let guard = state.lock().unwrap(); let guard = lock!(state);
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?; let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard); drop(guard);
Ok(cloned_state) Ok(cloned_state)

View File

@ -67,11 +67,15 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
#[tauri::command] #[tauri::command]
pub fn update_settings(new_settings: Value) { pub fn update_settings(new_settings: Value) {
let mut db_lock = borrow_db_mut_checked(); let mut db_lock = borrow_db_mut_checked();
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap(); let mut current_settings = serde_json::to_value(db_lock.settings.clone()).expect("Failed to parse existing settings");
for (key, value) in new_settings.as_object().unwrap() { let values = match new_settings.as_object() {
Some(values) => values,
None => { panic!("Could not parse settings values"); },
};
for (key, value) in values {
current_settings[key] = value.clone(); current_settings[key] = value.clone();
} }
let new_settings: Settings = serde_json::from_value(current_settings).unwrap(); let new_settings: Settings = serde_json::from_value(current_settings).unwrap_or_else(|e| panic!("Failed to parse settings with error {}", e));
db_lock.settings = new_settings; db_lock.settings = new_settings;
} }
#[tauri::command] #[tauri::command]

View File

@ -8,7 +8,6 @@ use std::{
use chrono::Utc; use chrono::Utc;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use url::Url; use url::Url;
@ -17,8 +16,18 @@ use crate::DB;
use super::models::data::Database; use super::models::data::Database;
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = #[cfg(not(debug_assertions))]
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop"))); static DATA_ROOT_PREFIX: &'static str = "drop";
#[cfg(debug_assertions)]
static DATA_ROOT_PREFIX: &str = "drop-debug";
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
Arc::new(
dirs::data_dir()
.expect("Failed to get data dir")
.join(DATA_ROOT_PREFIX),
)
});
// Custom JSON serializer to support everything we need // Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -28,7 +37,7 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer for DropDatabaseSerializer
{ {
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::rmp_serde_1_3::RmpSerde::encode(val) native_model::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string())) .map_err(|e| DeSerError::Internal(e.to_string()))
} }
@ -36,7 +45,7 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
let mut buf = Vec::new(); let mut buf = Vec::new();
s.read_to_end(&mut buf) s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?; .map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf) let (val, _version) = native_model::decode(buf)
.map_err(|e| DeSerError::Internal(e.to_string()))?; .map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val) Ok(val)
} }
@ -59,13 +68,49 @@ impl DatabaseImpls for DatabaseInterface {
let pfx_dir = DATA_ROOT_DIR.join("pfx"); let pfx_dir = DATA_ROOT_DIR.join("pfx");
debug!("creating data directory at {DATA_ROOT_DIR:?}"); debug!("creating data directory at {DATA_ROOT_DIR:?}");
create_dir_all(DATA_ROOT_DIR.as_path()).unwrap(); create_dir_all(DATA_ROOT_DIR.as_path()).unwrap_or_else(|e| {
create_dir_all(&games_base_dir).unwrap(); panic!(
create_dir_all(&logs_root_dir).unwrap(); "Failed to create directory {} with error {}",
create_dir_all(&cache_dir).unwrap(); DATA_ROOT_DIR.display(),
create_dir_all(&pfx_dir).unwrap(); e
)
});
create_dir_all(&games_base_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
games_base_dir.display(),
e
)
});
create_dir_all(&logs_root_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
logs_root_dir.display(),
e
)
});
create_dir_all(&cache_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
cache_dir.display(),
e
)
});
create_dir_all(&pfx_dir).unwrap_or_else(|e| {
panic!(
"Failed to create directory {} with error {}",
pfx_dir.display(),
e
)
});
let exists = fs::exists(db_path.clone()).unwrap(); let exists = fs::exists(db_path.clone()).unwrap_or_else(|e| {
panic!(
"Failed to find if {} exists with error {}",
db_path.display(),
e
)
});
if exists { if exists {
match PathDatabase::load_from_path(db_path.clone()) { match PathDatabase::load_from_path(db_path.clone()) {
@ -74,21 +119,19 @@ impl DatabaseImpls for DatabaseInterface {
} }
} else { } else {
let default = Database::new(games_base_dir, None, cache_dir); let default = Database::new(games_base_dir, None, cache_dir);
debug!( debug!("Creating database at path {}", db_path.display());
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default).expect("Database could not be created") PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
} }
} }
fn database_is_set_up(&self) -> bool { fn database_is_set_up(&self) -> bool {
!self.borrow_data().unwrap().base_url.is_empty() !borrow_db_checked().base_url.is_empty()
} }
fn fetch_base_url(&self) -> Url { fn fetch_base_url(&self) -> Url {
let handle = self.borrow_data().unwrap(); let handle = borrow_db_checked();
Url::parse(&handle.base_url).unwrap() Url::parse(&handle.base_url)
.unwrap_or_else(|_| panic!("Failed to parse base url {}", handle.base_url))
} }
} }
@ -107,13 +150,16 @@ fn handle_invalid_database(
base base
}; };
info!("old database stored at: {}", new_path.to_string_lossy()); info!("old database stored at: {}", new_path.to_string_lossy());
fs::rename(&db_path, &new_path).unwrap(); fs::rename(&db_path, &new_path).unwrap_or_else(|e| {
panic!(
"Could not rename database {} to {} with error {}",
db_path.display(),
new_path.display(),
e
)
});
let db = Database::new( let db = Database::new(games_base_dir, Some(new_path), cache_dir);
games_base_dir.into_os_string().into_string().unwrap(),
Some(new_path),
cache_dir,
);
PathDatabase::create_at_path(db_path, db).expect("Database could not be created") PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
} }

View File

@ -1,17 +1,12 @@
/**
* NEXT BREAKING CHANGE
*
* UPDATE DATABASE TO USE RPMSERDENAMED
*
* WE CAN'T DELETE ANY FIELDS
*/
pub mod data { pub mod data {
use std::path::PathBuf; use std::{hash::Hash, path::PathBuf};
use native_model::native_model; use native_model::native_model;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// NOTE: Within each version, you should NEVER use these types.
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
pub type GameVersion = v1::GameVersion; pub type GameVersion = v1::GameVersion;
pub type Database = v3::Database; pub type Database = v3::Database;
pub type Settings = v1::Settings; pub type Settings = v1::Settings;
@ -19,14 +14,29 @@ pub mod data {
pub type GameDownloadStatus = v2::GameDownloadStatus; pub type GameDownloadStatus = v2::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus; pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
/**
* Need to be universally accessible by the ID, and the version is just a couple sprinkles on top
*/
pub type DownloadableMetadata = v1::DownloadableMetadata; pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType; pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v2::DatabaseApplications; pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo; // pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::collections::HashMap; use std::collections::HashMap;
pub mod v1 { impl PartialEq for DownloadableMetadata {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.download_type == other.download_type
}
}
impl Hash for DownloadableMetadata {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.download_type.hash(state);
}
}
mod v1 {
use crate::process::process_manager::Platform; use crate::process::process_manager::Platform;
use serde_with::serde_as; use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
@ -116,6 +126,7 @@ pub mod data {
// Stuff that shouldn't be synced to disk // Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus { pub enum ApplicationTransientStatus {
Queued { version_name: String },
Downloading { version_name: String }, Downloading { version_name: String },
Uninstalling {}, Uninstalling {},
Updating { version_name: String }, Updating { version_name: String },
@ -144,7 +155,7 @@ pub mod data {
} }
#[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)] #[derive(Debug, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata { pub struct DownloadableMetadata {
pub id: String, pub id: String,
@ -174,22 +185,21 @@ pub mod data {
} }
} }
pub mod v2 { mod v2 {
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use serde_with::serde_as; use serde_with::serde_as;
use super::{ use super::{
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata, Deserialize, Serialize, native_model, v1,
GameVersion, Serialize, Settings, native_model, v1,
}; };
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::Database)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database { pub struct Database {
#[serde(default)] #[serde(default)]
pub settings: Settings, pub settings: v1::Settings,
pub auth: Option<DatabaseAuth>, pub auth: Option<v1::DatabaseAuth>,
pub base_url: String, pub base_url: String,
pub applications: v1::DatabaseApplications, pub applications: v1::DatabaseApplications,
#[serde(skip)] #[serde(skip)]
@ -198,7 +208,7 @@ pub mod data {
pub compat_info: Option<DatabaseCompatInfo>, pub compat_info: Option<DatabaseCompatInfo>,
} }
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo { pub struct DatabaseCompatInfo {
@ -221,7 +231,7 @@ pub mod data {
// Strings are version names for a particular game // Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)] #[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::GameDownloadStatus)]
pub enum GameDownloadStatus { pub enum GameDownloadStatus {
Remote {}, Remote {},
SetupRequired { SetupRequired {
@ -261,17 +271,17 @@ pub mod data {
#[serde_as] #[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)] #[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from=v1::DatabaseApplications)]
pub struct DatabaseApplications { pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>, pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map // Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>, pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>, pub game_versions: HashMap<String, HashMap<String, v1::GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>, pub installed_game_version: HashMap<String, v1::DownloadableMetadata>,
#[serde(skip)] #[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>, pub transient_statuses: HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
} }
impl From<v1::DatabaseApplications> for DatabaseApplications { impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self { fn from(value: v1::DatabaseApplications) -> Self {
@ -293,21 +303,21 @@ pub mod data {
use std::path::PathBuf; use std::path::PathBuf;
use super::{ use super::{
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize, Deserialize, Serialize,
Settings, native_model, v2, native_model, v2, v1,
}; };
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)] #[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde, from = v2::Database)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database { pub struct Database {
#[serde(default)] #[serde(default)]
pub settings: Settings, pub settings: v1::Settings,
pub auth: Option<DatabaseAuth>, pub auth: Option<v1::DatabaseAuth>,
pub base_url: String, pub base_url: String,
pub applications: DatabaseApplications, pub applications: v2::DatabaseApplications,
#[serde(skip)] #[serde(skip)]
pub prev_database: Option<PathBuf>, pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf, pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>, pub compat_info: Option<v2::DatabaseCompatInfo>,
} }
impl From<v2::Database> for Database { impl From<v2::Database> for Database {
@ -347,5 +357,6 @@ pub mod data {
compat_info: None, compat_info: None,
} }
} }
} }
} }

View File

@ -5,10 +5,10 @@ use log::warn;
use crate::{ use crate::{
database::{ database::{
db::borrow_db_mut_checked, db::borrow_db_mut_checked,
models::data::v1::{DownloadType, DownloadableMetadata}, models::data::{DownloadType, DownloadableMetadata},
}, },
games::{ games::{
downloads::drop_data::{v1::DropData, DROP_DATA_PATH}, downloads::drop_data::{DropData, DROP_DATA_PATH},
library::set_partially_installed_db, library::set_partially_installed_db,
}, },
}; };
@ -24,11 +24,11 @@ pub fn scan_install_dirs() {
if !drop_data_file.exists() { if !drop_data_file.exists() {
continue; continue;
} }
let game_id = game.file_name().into_string().unwrap(); let game_id = game.file_name().display().to_string();
let Ok(drop_data) = DropData::read(&game.path()) else { let Ok(drop_data) = DropData::read(&game.path()) else {
warn!( warn!(
".dropdata exists for {}, but couldn't read it. is it corrupted?", ".dropdata exists for {}, but couldn't read it. is it corrupted?",
game.file_name().into_string().unwrap() game.file_name().display()
); );
continue; continue;
}; };

View File

@ -1,15 +1,15 @@
use std::sync::Mutex; use std::sync::Mutex;
use crate::{database::models::data::DownloadableMetadata, AppState}; use crate::{AppState, database::models::data::DownloadableMetadata, lock};
#[tauri::command] #[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) { pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads(); lock!(state).download_manager.pause_downloads();
} }
#[tauri::command] #[tauri::command]
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) { pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads(); lock!(state).download_manager.resume_downloads();
} }
#[tauri::command] #[tauri::command]
@ -18,14 +18,12 @@ pub fn move_download_in_queue(
old_index: usize, old_index: usize,
new_index: usize, new_index: usize,
) { ) {
state lock!(state)
.lock()
.unwrap()
.download_manager .download_manager
.rearrange(old_index, new_index); .rearrange(old_index, new_index);
} }
#[tauri::command] #[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) { pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta); lock!(state).download_manager.cancel(meta);
} }

View File

@ -11,9 +11,7 @@ use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use crate::{ use crate::{
database::models::data::DownloadableMetadata, app_emit, database::models::data::DownloadableMetadata, download_manager::download_manager_frontend::DownloadStatus, error::application_download_error::ApplicationDownloadError, games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}, lock, send
error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
}; };
use super::{ use super::{
@ -75,7 +73,6 @@ pub struct DownloadManagerBuilder {
status: Arc<Mutex<DownloadManagerStatus>>, status: Arc<Mutex<DownloadManagerStatus>>,
app_handle: AppHandle, app_handle: AppHandle,
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
current_download_thread: Mutex<Option<JoinHandle<()>>>, current_download_thread: Mutex<Option<JoinHandle<()>>>,
active_control_flag: Option<DownloadThreadControl>, active_control_flag: Option<DownloadThreadControl>,
} }
@ -95,7 +92,6 @@ impl DownloadManagerBuilder {
progress: active_progress.clone(), progress: active_progress.clone(),
app_handle, app_handle,
current_download_agent: None,
current_download_thread: Mutex::new(None), current_download_thread: Mutex::new(None),
active_control_flag: None, active_control_flag: None,
}; };
@ -106,7 +102,7 @@ impl DownloadManagerBuilder {
} }
fn set_status(&self, status: DownloadManagerStatus) { fn set_status(&self, status: DownloadManagerStatus) {
*self.status.lock().unwrap() = status; *lock!(self.status) = status;
} }
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent { fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
@ -120,10 +116,9 @@ impl DownloadManagerBuilder {
// Make sure the download thread is terminated // Make sure the download thread is terminated
fn cleanup_current_download(&mut self) { fn cleanup_current_download(&mut self) {
self.active_control_flag = None; self.active_control_flag = None;
*self.progress.lock().unwrap() = None; *lock!(self.progress) = None;
self.current_download_agent = None;
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); let mut download_thread_lock = lock!(self.current_download_thread);
if let Some(unfinished_thread) = download_thread_lock.take() if let Some(unfinished_thread) = download_thread_lock.take()
&& !unfinished_thread.is_finished() && !unfinished_thread.is_finished()
@ -139,7 +134,7 @@ impl DownloadManagerBuilder {
current_flag.set(DownloadThreadControlFlag::Stop); current_flag.set(DownloadThreadControlFlag::Stop);
} }
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); let mut download_thread_lock = lock!(self.current_download_thread);
if let Some(current_download_thread) = download_thread_lock.take() { if let Some(current_download_thread) = download_thread_lock.take() {
return current_download_thread.join().is_ok(); return current_download_thread.join().is_ok();
}; };
@ -197,13 +192,11 @@ impl DownloadManagerBuilder {
return; return;
} }
download_agent.on_initialised(&self.app_handle); download_agent.on_queued(&self.app_handle);
self.download_queue.append(meta.clone()); self.download_queue.append(meta.clone());
self.download_agent_registry.insert(meta, download_agent); self.download_agent_registry.insert(meta, download_agent);
self.sender send!(self.sender, DownloadManagerSignal::UpdateUIQueue);
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
} }
fn manage_go_signal(&mut self) { fn manage_go_signal(&mut self) {
@ -216,19 +209,13 @@ impl DownloadManagerBuilder {
return; return;
} }
if self.current_download_agent.is_some()
&& self.download_queue.read().front().unwrap()
== &self.current_download_agent.as_ref().unwrap().metadata()
{
return;
}
debug!("current download queue: {:?}", self.download_queue.read()); debug!("current download queue: {:?}", self.download_queue.read());
// Should always be Some if the above two statements keep going let agent_data = if let Some(agent_data) = self.download_queue.read().front() {
let agent_data = self.download_queue.read().front().unwrap().clone(); agent_data.clone()
} else {
info!("starting download for {agent_data:?}"); return;
};
let download_agent = self let download_agent = self
.download_agent_registry .download_agent_registry
@ -236,12 +223,26 @@ impl DownloadManagerBuilder {
.unwrap() .unwrap()
.clone(); .clone();
let status = download_agent.status();
// This download is already going
if status != DownloadStatus::Queued {
return;
}
// Ensure all others are marked as queued
for agent in self.download_agent_registry.values() {
if agent.metadata() != agent_data && agent.status() != DownloadStatus::Queued {
agent.on_queued(&self.app_handle);
}
}
info!("starting download for {agent_data:?}");
self.active_control_flag = Some(download_agent.control_flag()); self.active_control_flag = Some(download_agent.control_flag());
self.current_download_agent = Some(download_agent.clone());
let sender = self.sender.clone(); let sender = self.sender.clone();
let mut download_thread_lock = self.current_download_thread.lock().unwrap(); let mut download_thread_lock = lock!(self.current_download_thread);
let app_handle = self.app_handle.clone(); let app_handle = self.app_handle.clone();
*download_thread_lock = Some(spawn(move || { *download_thread_lock = Some(spawn(move || {
@ -252,7 +253,7 @@ impl DownloadManagerBuilder {
Err(e) => { Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e); error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, &e); download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap(); send!(sender, DownloadManagerSignal::Error(e));
return; return;
} }
}; };
@ -276,7 +277,7 @@ impl DownloadManagerBuilder {
&e &e
); );
download_agent.on_error(&app_handle, &e); download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap(); send!(sender, DownloadManagerSignal::Error(e));
return; return;
} }
}; };
@ -287,10 +288,8 @@ impl DownloadManagerBuilder {
if validate_result { if validate_result {
download_agent.on_complete(&app_handle); download_agent.on_complete(&app_handle);
sender send!(sender, DownloadManagerSignal::Completed(download_agent.metadata()));
.send(DownloadManagerSignal::Completed(download_agent.metadata())) send!(sender, DownloadManagerSignal::UpdateUIQueue);
.unwrap();
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
return; return;
} }
} }
@ -310,22 +309,24 @@ impl DownloadManagerBuilder {
} }
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) { fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
debug!("got signal Completed"); debug!("got signal Completed");
if let Some(interface) = &self.current_download_agent if let Some(interface) = self.download_queue.read().front()
&& interface.metadata() == meta && interface == &meta
{ {
self.remove_and_cleanup_front_download(&meta); self.remove_and_cleanup_front_download(&meta);
} }
self.push_ui_queue_update(); self.push_ui_queue_update();
self.sender.send(DownloadManagerSignal::Go).unwrap(); send!(self.sender, DownloadManagerSignal::Go);
} }
fn manage_error_signal(&mut self, error: ApplicationDownloadError) { fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
debug!("got signal Error"); debug!("got signal Error");
if let Some(current_agent) = self.current_download_agent.clone() { if let Some(metadata) = self.download_queue.read().front()
&& let Some(current_agent) = self.download_agent_registry.get(metadata)
{
current_agent.on_error(&self.app_handle, &error); current_agent.on_error(&self.app_handle, &error);
self.stop_and_wait_current_download(); self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata()); self.remove_and_cleanup_front_download(metadata);
} }
self.push_ui_queue_update(); self.push_ui_queue_update();
self.set_status(DownloadManagerStatus::Error); self.set_status(DownloadManagerStatus::Error);
@ -333,8 +334,11 @@ impl DownloadManagerBuilder {
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
debug!("got signal Cancel"); debug!("got signal Cancel");
if let Some(current_download) = &self.current_download_agent { // If the current download is the one we're tryna cancel
if &current_download.metadata() == meta { if let Some(current_metadata) = self.download_queue.read().front()
&& current_metadata == meta
&& let Some(current_download) = self.download_agent_registry.get(current_metadata)
{
self.set_status(DownloadManagerStatus::Paused); self.set_status(DownloadManagerStatus::Paused);
current_download.on_cancelled(&self.app_handle); current_download.on_cancelled(&self.app_handle);
self.stop_and_wait_current_download(); self.stop_and_wait_current_download();
@ -342,27 +346,15 @@ impl DownloadManagerBuilder {
self.download_queue.pop_front(); self.download_queue.pop_front();
self.cleanup_current_download(); self.cleanup_current_download();
self.download_agent_registry.remove(meta);
debug!("current download queue: {:?}", self.download_queue.read()); debug!("current download queue: {:?}", self.download_queue.read());
} }
// TODO: Collapse these two into a single if statement somehow // else just cancel it
else if let Some(download_agent) = self.download_agent_registry.get(meta) { else if let Some(download_agent) = self.download_agent_registry.get(meta) {
let index = self.download_queue.get_by_meta(meta); let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index { if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle); download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap(); let _ = self.download_queue.edit().remove(index);
let removed = self.download_agent_registry.remove(meta);
debug!(
"removed {:?} from queue {:?}",
removed.map(|x| x.metadata()),
self.download_queue.read()
);
}
}
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta); let removed = self.download_agent_registry.remove(meta);
debug!( debug!(
"removed {:?} from queue {:?}", "removed {:?} from queue {:?}",
@ -371,12 +363,13 @@ impl DownloadManagerBuilder {
); );
} }
} }
self.sender.send(DownloadManagerSignal::Go).unwrap();
self.push_ui_queue_update(); self.push_ui_queue_update();
} }
fn push_ui_stats_update(&self, kbs: usize, time: usize) { fn push_ui_stats_update(&self, kbs: usize, time: usize) {
let event_data = StatsUpdateEvent { speed: kbs, time }; let event_data = StatsUpdateEvent { speed: kbs, time };
self.app_handle.emit("update_stats", event_data).unwrap(); app_emit!(self.app_handle, "update_stats", event_data);
} }
fn push_ui_queue_update(&self) { fn push_ui_queue_update(&self) {
let queue = &self.download_queue.read(); let queue = &self.download_queue.read();
@ -395,6 +388,6 @@ impl DownloadManagerBuilder {
.collect(); .collect();
let event_data = QueueUpdateEvent { queue: queue_objs }; let event_data = QueueUpdateEvent { queue: queue_objs };
self.app_handle.emit("update_queue", event_data).unwrap(); app_emit!(self.app_handle, "update_queue", event_data);
} }
} }

View File

@ -3,8 +3,8 @@ use std::{
collections::VecDeque, collections::VecDeque,
fmt::Debug, fmt::Debug,
sync::{ sync::{
mpsc::{SendError, Sender},
Mutex, MutexGuard, Mutex, MutexGuard,
mpsc::{SendError, Sender},
}, },
thread::JoinHandle, thread::JoinHandle,
}; };
@ -14,7 +14,7 @@ use serde::Serialize;
use crate::{ use crate::{
database::models::data::DownloadableMetadata, database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError, error::application_download_error::ApplicationDownloadError, lock, send,
}; };
use super::{ use super::{
@ -62,7 +62,7 @@ impl Serialize for DownloadManagerStatus {
} }
} }
#[derive(Serialize, Clone, Debug)] #[derive(Serialize, Clone, Debug, PartialEq)]
pub enum DownloadStatus { pub enum DownloadStatus {
Queued, Queued,
Downloading, Downloading,
@ -119,22 +119,18 @@ impl DownloadManager {
self.download_queue.read() self.download_queue.read()
} }
pub fn get_current_download_progress(&self) -> Option<f64> { pub fn get_current_download_progress(&self) -> Option<f64> {
let progress_object = (*self.progress.lock().unwrap()).clone()?; let progress_object = (*lock!(self.progress)).clone()?;
Some(progress_object.get_progress()) Some(progress_object.get_progress())
} }
pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) { pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
let mut queue = self.edit(); let mut queue = self.edit();
let current_index = get_index_from_id(&mut queue, meta).unwrap(); let current_index = get_index_from_id(&mut queue, meta).expect("Failed to get meta index from id");
let to_move = queue.remove(current_index).unwrap(); let to_move = queue.remove(current_index).expect("Failed to remove meta at index from queue");
queue.insert(new_index, to_move); queue.insert(new_index, to_move);
self.command_sender send!(self.command_sender, DownloadManagerSignal::UpdateUIQueue);
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
} }
pub fn cancel(&self, meta: DownloadableMetadata) { pub fn cancel(&self, meta: DownloadableMetadata) {
self.command_sender send!(self.command_sender, DownloadManagerSignal::Cancel(meta));
.send(DownloadManagerSignal::Cancel(meta))
.unwrap();
} }
pub fn rearrange(&self, current_index: usize, new_index: usize) { pub fn rearrange(&self, current_index: usize, new_index: usize) {
if current_index == new_index { if current_index == new_index {
@ -143,39 +139,31 @@ impl DownloadManager {
let needs_pause = current_index == 0 || new_index == 0; let needs_pause = current_index == 0 || new_index == 0;
if needs_pause { if needs_pause {
self.command_sender send!(self.command_sender, DownloadManagerSignal::Stop);
.send(DownloadManagerSignal::Stop)
.unwrap();
} }
debug!("moving download at index {current_index} to index {new_index}"); debug!("moving download at index {current_index} to index {new_index}");
let mut queue = self.edit(); let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap(); let to_move = queue.remove(current_index).expect("Failed to get");
queue.insert(new_index, to_move); queue.insert(new_index, to_move);
drop(queue); drop(queue);
if needs_pause { if needs_pause {
self.command_sender.send(DownloadManagerSignal::Go).unwrap(); send!(self.command_sender, DownloadManagerSignal::Go);
} }
self.command_sender send!(self.command_sender, DownloadManagerSignal::UpdateUIQueue);
.send(DownloadManagerSignal::UpdateUIQueue) send!(self.command_sender, DownloadManagerSignal::Go);
.unwrap();
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
} }
pub fn pause_downloads(&self) { pub fn pause_downloads(&self) {
self.command_sender send!(self.command_sender, DownloadManagerSignal::Stop);
.send(DownloadManagerSignal::Stop)
.unwrap();
} }
pub fn resume_downloads(&self) { pub fn resume_downloads(&self) {
self.command_sender.send(DownloadManagerSignal::Go).unwrap(); send!(self.command_sender, DownloadManagerSignal::Go);
} }
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> { pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
self.command_sender send!(self.command_sender, DownloadManagerSignal::Finish);
.send(DownloadManagerSignal::Finish) let terminator = lock!(self.terminator).take();
.unwrap();
let terminator = self.terminator.lock().unwrap().take();
terminator.unwrap().join() terminator.unwrap().join()
} }
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> { pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {

View File

@ -12,6 +12,12 @@ use super::{
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject}, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
}; };
/**
* Downloadables are responsible for managing their specific object's download state
* e.g, the GameDownloadAgent is responsible for pushing game updates
*
* But the download manager manages the queue state
*/
pub trait Downloadable: Send + Sync { pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>; fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>; fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
@ -20,7 +26,7 @@ pub trait Downloadable: Send + Sync {
fn control_flag(&self) -> DownloadThreadControl; fn control_flag(&self) -> DownloadThreadControl;
fn status(&self) -> DownloadStatus; fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata; fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle); fn on_queued(&self, app_handle: &AppHandle);
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError); fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
fn on_complete(&self, app_handle: &AppHandle); fn on_complete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle); fn on_cancelled(&self, app_handle: &AppHandle);

View File

@ -10,7 +10,7 @@ use std::{
use atomic_instant_full::AtomicInstant; use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle; use throttle_my_fn::throttle;
use crate::download_manager::download_manager_frontend::DownloadManagerSignal; use crate::{download_manager::download_manager_frontend::DownloadManagerSignal, lock, send};
use super::rolling_progress_updates::RollingProgressWindow; use super::rolling_progress_updates::RollingProgressWindow;
@ -23,7 +23,7 @@ pub struct ProgressObject {
//last_update: Arc<RwLock<Instant>>, //last_update: Arc<RwLock<Instant>>,
last_update_time: Arc<AtomicInstant>, last_update_time: Arc<AtomicInstant>,
bytes_last_update: Arc<AtomicUsize>, bytes_last_update: Arc<AtomicUsize>,
rolling: RollingProgressWindow<1>, rolling: RollingProgressWindow<1000>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -74,12 +74,10 @@ impl ProgressObject {
} }
pub fn set_time_now(&self) { pub fn set_time_now(&self) {
*self.start.lock().unwrap() = Instant::now(); *lock!(self.start) = Instant::now();
} }
pub fn sum(&self) -> usize { pub fn sum(&self) -> usize {
self.progress_instances lock!(self.progress_instances)
.lock()
.unwrap()
.iter() .iter()
.map(|instance| instance.load(Ordering::Acquire)) .map(|instance| instance.load(Ordering::Acquire))
.sum() .sum()
@ -88,27 +86,25 @@ impl ProgressObject {
self.set_time_now(); self.set_time_now();
self.bytes_last_update.store(0, Ordering::Release); self.bytes_last_update.store(0, Ordering::Release);
self.rolling.reset(); self.rolling.reset();
self.progress_instances lock!(self.progress_instances)
.lock()
.unwrap()
.iter() .iter()
.for_each(|x| x.store(0, Ordering::SeqCst)); .for_each(|x| x.store(0, Ordering::SeqCst));
} }
pub fn get_max(&self) -> usize { pub fn get_max(&self) -> usize {
*self.max.lock().unwrap() *lock!(self.max)
} }
pub fn set_max(&self, new_max: usize) { pub fn set_max(&self, new_max: usize) {
*self.max.lock().unwrap() = new_max; *lock!(self.max) = new_max;
} }
pub fn set_size(&self, length: usize) { pub fn set_size(&self, length: usize) {
*self.progress_instances.lock().unwrap() = *lock!(self.progress_instances) =
(0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
} }
pub fn get_progress(&self) -> f64 { pub fn get_progress(&self) -> f64 {
self.sum() as f64 / self.get_max() as f64 self.sum() as f64 / self.get_max() as f64
} }
pub fn get(&self, index: usize) -> Arc<AtomicUsize> { pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
self.progress_instances.lock().unwrap()[index].clone() lock!(self.progress_instances)[index].clone()
} }
fn update_window(&self, kilobytes_per_second: usize) { fn update_window(&self, kilobytes_per_second: usize) {
self.rolling.update(kilobytes_per_second); self.rolling.update(kilobytes_per_second);
@ -120,7 +116,7 @@ pub fn calculate_update(progress: &ProgressObject) {
let last_update_time = progress let last_update_time = progress
.last_update_time .last_update_time
.swap(Instant::now(), Ordering::SeqCst); .swap(Instant::now(), Ordering::SeqCst);
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis(); let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis_f64();
let current_bytes_downloaded = progress.sum(); let current_bytes_downloaded = progress.sum();
let max = progress.get_max(); let max = progress.get_max();
@ -128,17 +124,17 @@ pub fn calculate_update(progress: &ProgressObject) {
.bytes_last_update .bytes_last_update
.swap(current_bytes_downloaded, Ordering::Acquire); .swap(current_bytes_downloaded, Ordering::Acquire);
let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update); let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update) as f64;
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1); let kilobytes_per_second = bytes_since_last_update / time_since_last_update;
let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes
progress.update_window(kilobytes_per_second); progress.update_window(kilobytes_per_second as usize);
push_update(progress, bytes_remaining); push_update(progress, bytes_remaining);
} }
#[throttle(1, Duration::from_millis(500))] #[throttle(1, Duration::from_millis(250))]
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) { pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
let average_speed = progress.rolling.get_average(); let average_speed = progress.rolling.get_average();
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1); let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
@ -148,18 +144,12 @@ pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
} }
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) { fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
progress_object send!(
.sender progress_object.sender,
.send(DownloadManagerSignal::UpdateUIStats( DownloadManagerSignal::UpdateUIStats(kilobytes_per_second, time_remaining)
kilobytes_per_second, );
time_remaining,
))
.unwrap();
} }
fn update_queue(progress: &ProgressObject) { fn update_queue(progress: &ProgressObject) {
progress send!(progress.sender, DownloadManagerSignal::UpdateUIQueue)
.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
} }

View File

@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use crate::database::models::data::DownloadableMetadata; use crate::{database::models::data::DownloadableMetadata, lock};
#[derive(Clone)] #[derive(Clone)]
pub struct Queue { pub struct Queue {
@ -24,10 +24,10 @@ impl Queue {
} }
} }
pub fn read(&self) -> VecDeque<DownloadableMetadata> { pub fn read(&self) -> VecDeque<DownloadableMetadata> {
self.inner.lock().unwrap().clone() lock!(self.inner).clone()
} }
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> { pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
self.inner.lock().unwrap() lock!(self.inner)
} }
pub fn pop_front(&self) -> Option<DownloadableMetadata> { pub fn pop_front(&self) -> Option<DownloadableMetadata> {
self.edit().pop_front() self.edit().pop_front()

View File

@ -1,6 +1,6 @@
use std::sync::{ use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Arc,
atomic::{AtomicUsize, Ordering},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -22,17 +22,22 @@ impl<const S: usize> RollingProgressWindow<S> {
} }
pub fn get_average(&self) -> usize { pub fn get_average(&self) -> usize {
let current = self.current.load(Ordering::SeqCst); let current = self.current.load(Ordering::SeqCst);
self.window let valid = self
.window
.iter() .iter()
.enumerate() .enumerate()
.filter(|(i, _)| i < &current) .filter(|(i, _)| i < &current)
.map(|(_, x)| x.load(Ordering::Acquire)) .map(|(_, x)| x.load(Ordering::Acquire))
.sum::<usize>() .collect::<Vec<usize>>();
/ S let amount = valid.len();
let sum = valid.into_iter().sum::<usize>();
sum / amount
} }
pub fn reset(&self) { pub fn reset(&self) {
self.window self.window
.iter() .iter()
.for_each(|x| x.store(0, Ordering::Release)); .for_each(|x| x.store(0, Ordering::Release));
self.current.store(0, Ordering::Release);
} }
} }

View File

@ -18,7 +18,7 @@ pub enum ApplicationDownloadError {
Checksum, Checksum,
Lock, Lock,
IoError(Arc<io::Error>), IoError(Arc<io::Error>),
DownloadError, DownloadError(RemoteAccessError),
} }
impl Display for ApplicationDownloadError { impl Display for ApplicationDownloadError {
@ -40,10 +40,16 @@ impl Display for ApplicationDownloadError {
write!(f, "checksum failed to validate for download") write!(f, "checksum failed to validate for download")
} }
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"), ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
ApplicationDownloadError::DownloadError => write!( ApplicationDownloadError::DownloadError(error) => write!(
f, f,
"Download failed. See Download Manager status for specific error" "Download failed with error {error}"
), ),
} }
} }
} }
impl From<io::Error> for ApplicationDownloadError {
fn from(value: io::Error) -> Self {
ApplicationDownloadError::IoError(Arc::new(value))
}
}

View File

@ -0,0 +1,26 @@
use std::fmt::Display;
use http::{header::ToStrError, HeaderName};
use serde_with::SerializeDisplay;
use crate::error::remote_access_error::RemoteAccessError;
#[derive(Debug, SerializeDisplay)]
pub enum CacheError {
HeaderNotFound(HeaderName),
ParseError(ToStrError),
Remote(RemoteAccessError),
ConstructionError(http::Error)
}
impl Display for CacheError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
CacheError::HeaderNotFound(header_name) => format!("Could not find header {header_name} in cache"),
CacheError::ParseError(to_str_error) => format!("Could not parse cache with error {to_str_error}"),
CacheError::Remote(remote_access_error) => format!("Cache got remote access error: {remote_access_error}"),
CacheError::ConstructionError(error) => format!("Could not construct cache body with error {error}"),
};
write!(f, "{s}")
}
}

View File

@ -1,18 +1,21 @@
use std::fmt::Display; use std::fmt::{Display};
use serde_with::SerializeDisplay; use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)] #[derive(SerializeDisplay)]
pub enum LibraryError { pub enum LibraryError {
MetaNotFound(String), MetaNotFound(String),
VersionNotFound(String),
} }
impl Display for LibraryError { impl Display for LibraryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { write!(f, "{}", match self {
LibraryError::MetaNotFound(id) => write!( LibraryError::MetaNotFound(id) => {
f, format!("Could not locate any installed version of game ID {id} in the database")
"Could not locate any installed version of game ID {id} in the database" }
), LibraryError::VersionNotFound(game_id) => {
} format!("Could not locate any installed version for game id {game_id} in the database")
}
})
} }
} }

View File

@ -4,3 +4,4 @@ pub mod drop_server_error;
pub mod library_error; pub mod library_error;
pub mod process_error; pub mod process_error;
pub mod remote_access_error; pub mod remote_access_error;
pub mod cache_error;

View File

@ -11,7 +11,8 @@ pub enum ProcessError {
IOError(Error), IOError(Error),
FormatError(String), // String errors supremacy FormatError(String), // String errors supremacy
InvalidPlatform, InvalidPlatform,
OpenerError(tauri_plugin_opener::Error) OpenerError(tauri_plugin_opener::Error),
InvalidArguments(String)
} }
impl Display for ProcessError { impl Display for ProcessError {
@ -23,8 +24,9 @@ impl Display for ProcessError {
ProcessError::InvalidVersion => "Invalid game version", ProcessError::InvalidVersion => "Invalid game version",
ProcessError::IOError(error) => &error.to_string(), ProcessError::IOError(error) => &error.to_string(),
ProcessError::InvalidPlatform => "This game cannot be played on the current platform", ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
ProcessError::FormatError(e) => &format!("Failed to format template: {e}"), ProcessError::FormatError(e) => &format!("Could not format template: {e}"),
ProcessError::OpenerError(error) => &format!("Failed to open directory: {error}"), ProcessError::OpenerError(error) => &format!("Could not open directory: {error}"),
ProcessError::InvalidArguments(arguments) => &format!("Invalid arguments in command {arguments}"),
}; };
write!(f, "{s}") write!(f, "{s}")
} }

View File

@ -44,8 +44,7 @@ impl Display for RemoteAccessError {
error error
.source() .source()
.map(std::string::ToString::to_string) .map(std::string::ToString::to_string)
.or_else(|| Some("Unknown error".to_string())) .unwrap_or("Unknown error".to_string())
.unwrap()
) )
} }
RemoteAccessError::FetchErrorWS(error) => write!( RemoteAccessError::FetchErrorWS(error) => write!(
@ -54,9 +53,8 @@ impl Display for RemoteAccessError {
error, error,
error error
.source() .source()
.map(|e| e.to_string()) .map(std::string::ToString::to_string)
.or_else(|| Some("Unknown error".to_string())) .unwrap_or("Unknown error".to_string())
.unwrap()
), ),
RemoteAccessError::ParsingError(parse_error) => { RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{parse_error}") write!(f, "{parse_error}")

View File

@ -1,10 +1,11 @@
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::games::library::Game; use crate::games::library::Game;
pub type Collections = Vec<Collection>; pub type Collections = Vec<Collection>;
#[derive(Serialize, Deserialize, Debug, Clone, Default)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Collection { pub struct Collection {
id: String, id: String,
@ -14,7 +15,7 @@ pub struct Collection {
entries: Vec<CollectionObject>, entries: Vec<CollectionObject>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Default)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CollectionObject { pub struct CollectionObject {
collection_id: String, collection_id: String,

View File

@ -4,6 +4,7 @@ use crate::{
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
remote::{ remote::{
auth::generate_authorization_header, auth::generate_authorization_header,
cache::{cache_object, get_cached_object},
requests::{generate_url, make_authenticated_get}, requests::{generate_url, make_authenticated_get},
utils::DROP_CLIENT_ASYNC, utils::DROP_CLIENT_ASYNC,
}, },
@ -12,11 +13,23 @@ use crate::{
use super::collection::{Collection, Collections}; use super::collection::{Collection, Collections};
#[tauri::command] #[tauri::command]
pub async fn fetch_collections() -> Result<Collections, RemoteAccessError> { pub async fn fetch_collections(
hard_refresh: Option<bool>,
) -> Result<Collections, RemoteAccessError> {
let do_hard_refresh = hard_refresh.unwrap_or(false);
if !do_hard_refresh && let Ok(cached_response) = get_cached_object::<Collections>("collections")
{
return Ok(cached_response);
}
let response = let response =
make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?; make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?;
Ok(response.json().await?) let collections: Collections = response.json().await?;
cache_object("collections", &collections)?;
Ok(collections)
} }
#[tauri::command] #[tauri::command]
@ -90,7 +103,8 @@ pub async fn delete_game_in_collection(
.delete(url) .delete(url)
.header("Authorization", generate_authorization_header()) .header("Authorization", generate_authorization_header())
.json(&json!({"id": game_id})) .json(&json!({"id": game_id}))
.send().await?; .send()
.await?;
Ok(()) Ok(())
} }

View File

@ -27,12 +27,14 @@ use super::{
#[tauri::command] #[tauri::command]
pub async fn fetch_library( pub async fn fetch_library(
state: tauri::State<'_, Mutex<AppState<'_>>>, state: tauri::State<'_, Mutex<AppState<'_>>>,
hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> { ) -> Result<Vec<Game>, RemoteAccessError> {
offline!( offline!(
state, state,
fetch_library_logic, fetch_library_logic,
fetch_library_logic_offline, fetch_library_logic_offline,
state state,
hard_refresh
).await ).await
} }

View File

@ -5,13 +5,10 @@ use std::{
use crate::{ use crate::{
AppState,
database::{ database::{
db::borrow_db_checked, db::borrow_db_checked,
models::data::GameDownloadStatus, models::data::GameDownloadStatus,
}, }, download_manager::downloadable::Downloadable, error::application_download_error::ApplicationDownloadError, lock, AppState
download_manager::downloadable::Downloadable,
error::application_download_error::ApplicationDownloadError,
}; };
use super::download_agent::GameDownloadAgent; use super::download_agent::GameDownloadAgent;
@ -23,16 +20,14 @@ pub async fn download_game(
install_dir: usize, install_dir: usize,
state: tauri::State<'_, Mutex<AppState<'_>>>, state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<(), ApplicationDownloadError> { ) -> Result<(), ApplicationDownloadError> {
let sender = { state.lock().unwrap().download_manager.get_sender().clone() }; let sender = { lock!(state).download_manager.get_sender().clone() };
let game_download_agent = let game_download_agent =
GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?; GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?;
let game_download_agent = let game_download_agent =
Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>); Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
state lock!(state)
.lock()
.unwrap()
.download_manager .download_manager
.queue_download(game_download_agent.clone()) .queue_download(game_download_agent.clone())
.unwrap(); .unwrap();
@ -62,22 +57,20 @@ pub async fn resume_download(
} => (version_name, install_dir), } => (version_name, install_dir),
}; };
let sender = state.lock().unwrap().download_manager.get_sender(); let sender = lock!(state).download_manager.get_sender();
let parent_dir: PathBuf = install_dir.into(); let parent_dir: PathBuf = install_dir.into();
let game_download_agent = Arc::new(Box::new( let game_download_agent = Arc::new(Box::new(
GameDownloadAgent::new( GameDownloadAgent::new(
game_id, game_id,
version_name.clone(), version_name.clone(),
parent_dir.parent().unwrap().to_path_buf(), parent_dir.parent().unwrap_or_else(|| panic!("Failed to get parent directry of {}", parent_dir.display())).to_path_buf(),
sender, sender,
) )
.await?, .await?,
) as Box<dyn Downloadable + Send + Sync>); ) as Box<dyn Downloadable + Send + Sync>);
state lock!(state)
.lock()
.unwrap()
.download_manager .download_manager
.queue_download(game_download_agent) .queue_download(game_download_agent)
.unwrap(); .unwrap();

View File

@ -20,10 +20,12 @@ use crate::games::state::GameStatusManager;
use crate::process::utils::get_disk_available; use crate::process::utils::get_disk_available;
use crate::remote::requests::generate_url; use crate::remote::requests::generate_url;
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}; use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
use crate::{app_emit, lock, send};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use rayon::ThreadPoolBuilder; use rayon::ThreadPoolBuilder;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs::{create_dir_all, OpenOptions}; use std::fs::{OpenOptions, create_dir_all};
use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -39,6 +41,7 @@ use super::drop_data::DropData;
static RETRY_COUNT: usize = 3; static RETRY_COUNT: usize = 3;
const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000; const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000;
const MAX_FILES_PER_BUCKET: usize = (1024 / 4) - 1;
pub struct GameDownloadAgent { pub struct GameDownloadAgent {
pub id: String, pub id: String,
@ -83,6 +86,8 @@ impl GameDownloadAgent {
let stored_manifest = let stored_manifest =
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone()); DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
let context_lock = stored_manifest.contexts.lock().unwrap().clone();
let result = Self { let result = Self {
id, id,
version, version,
@ -98,14 +103,19 @@ impl GameDownloadAgent {
result.ensure_manifest_exists().await?; result.ensure_manifest_exists().await?;
let required_space = result let required_space = lock!(result
.manifest .manifest)
.lock()
.unwrap()
.as_ref() .as_ref()
.unwrap() .unwrap()
.values() .values()
.map(|e| e.lengths.iter().sum::<usize>()) .map(|e| {
e.lengths
.iter()
.enumerate()
.filter(|(i, _)| *context_lock.get(&e.checksums[*i]).unwrap_or(&false))
.map(|(_, v)| v)
.sum::<usize>()
})
.sum::<usize>() as u64; .sum::<usize>() as u64;
let available_space = get_disk_available(data_base_dir_path)? as u64; let available_space = get_disk_available(data_base_dir_path)? as u64;
@ -162,11 +172,11 @@ impl GameDownloadAgent {
} }
pub fn check_manifest_exists(&self) -> bool { pub fn check_manifest_exists(&self) -> bool {
self.manifest.lock().unwrap().is_some() lock!(self.manifest).is_some()
} }
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> { pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
if self.manifest.lock().unwrap().is_some() { if lock!(self.manifest).is_some() {
return Ok(()); return Ok(());
} }
@ -197,7 +207,10 @@ impl GameDownloadAgent {
)); ));
} }
let manifest_download: DropManifest = response.json().await.unwrap(); let manifest_download: DropManifest = response
.json()
.await
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if let Ok(mut manifest) = self.manifest.lock() { if let Ok(mut manifest) = self.manifest.lock() {
*manifest = Some(manifest_download); *manifest = Some(manifest_download);
@ -209,7 +222,7 @@ impl GameDownloadAgent {
// Sets it up for both download and validate // Sets it up for both download and validate
fn setup_progress(&self) { fn setup_progress(&self) {
let buckets = self.buckets.lock().unwrap(); let buckets = lock!(self.buckets);
let chunk_count = buckets.iter().map(|e| e.drops.len()).sum(); let chunk_count = buckets.iter().map(|e| e.drops.len()).sum();
@ -224,21 +237,23 @@ impl GameDownloadAgent {
} }
pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> { pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> {
if self.buckets.lock().unwrap().is_empty() { if lock!(self.buckets).is_empty() {
self.generate_buckets()?; self.generate_buckets()?;
} }
*self.context_map.lock().unwrap() = self.dropdata.get_contexts(); *lock!(self.context_map) = self.dropdata.get_contexts();
Ok(()) Ok(())
} }
pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> { pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> {
let manifest = self.manifest.lock().unwrap().clone().unwrap(); let manifest = lock!(self.manifest)
.clone()
.ok_or(ApplicationDownloadError::NotInitialized)?;
let game_id = self.id.clone(); let game_id = self.id.clone();
let base_path = Path::new(&self.dropdata.base_path); let base_path = Path::new(&self.dropdata.base_path);
create_dir_all(base_path).unwrap(); create_dir_all(base_path)?;
let mut buckets = Vec::new(); let mut buckets = Vec::new();
@ -248,8 +263,13 @@ impl GameDownloadAgent {
for (raw_path, chunk) in manifest { for (raw_path, chunk) in manifest {
let path = base_path.join(Path::new(&raw_path)); let path = base_path.join(Path::new(&raw_path));
let container = path.parent().unwrap(); let container = path
create_dir_all(container).unwrap(); .parent()
.ok_or(ApplicationDownloadError::IoError(Arc::new(io::Error::new(
io::ErrorKind::NotFound,
"no parent directory",
))))?;
create_dir_all(container)?;
let already_exists = path.exists(); let already_exists = path.exists();
let file = OpenOptions::new() let file = OpenOptions::new()
@ -257,8 +277,7 @@ impl GameDownloadAgent {
.write(true) .write(true)
.create(true) .create(true)
.truncate(false) .truncate(false)
.open(path.clone()) .open(&path)?;
.unwrap();
let mut file_running_offset = 0; let mut file_running_offset = 0;
for (index, length) in chunk.lengths.iter().enumerate() { for (index, length) in chunk.lengths.iter().enumerate() {
@ -298,7 +317,8 @@ impl GameDownloadAgent {
drops: vec![], drops: vec![],
}); });
if *current_bucket_size + length >= TARGET_BUCKET_SIZE if (*current_bucket_size + length >= TARGET_BUCKET_SIZE
|| current_bucket.drops.len() >= MAX_FILES_PER_BUCKET)
&& !current_bucket.drops.is_empty() && !current_bucket.drops.is_empty()
{ {
// Move current bucket into list and make a new one // Move current bucket into list and make a new one
@ -341,7 +361,7 @@ impl GameDownloadAgent {
.collect::<Vec<(String, bool)>>(), .collect::<Vec<(String, bool)>>(),
); );
*self.buckets.lock().unwrap() = buckets; *lock!(self.buckets) = buckets;
Ok(()) Ok(())
} }
@ -357,9 +377,11 @@ impl GameDownloadAgent {
let pool = ThreadPoolBuilder::new() let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads) .num_threads(max_download_threads)
.build() .build()
.unwrap(); .unwrap_or_else(|_| {
panic!("failed to build thread pool with {max_download_threads} threads")
});
let buckets = self.buckets.lock().unwrap(); let buckets = lock!(self.buckets);
let mut download_contexts = HashMap::<String, DownloadContext>::new(); let mut download_contexts = HashMap::<String, DownloadContext>::new();
@ -367,7 +389,8 @@ impl GameDownloadAgent {
.iter() .iter()
.map(|e| &e.version) .map(|e| &e.version)
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
.into_iter().cloned() .into_iter()
.cloned()
.collect::<Vec<String>>(); .collect::<Vec<String>>();
info!("downloading across these versions: {versions:?}"); info!("downloading across these versions: {versions:?}");
@ -377,7 +400,7 @@ impl GameDownloadAgent {
for version in versions { for version in versions {
let download_context = DROP_CLIENT_SYNC let download_context = DROP_CLIENT_SYNC
.post(generate_url(&["/api/v2/client/context"], &[]).unwrap()) .post(generate_url(&["/api/v2/client/context"], &[])?)
.json(&ManifestBody { .json(&ManifestBody {
game: self.id.clone(), game: self.id.clone(),
version: version.clone(), version: version.clone(),
@ -400,7 +423,7 @@ impl GameDownloadAgent {
let download_contexts = &download_contexts; let download_contexts = &download_contexts;
pool.scope(|scope| { pool.scope(|scope| {
let context_map = self.context_map.lock().unwrap(); let context_map = lock!(self.context_map);
for (index, bucket) in buckets.iter().enumerate() { for (index, bucket) in buckets.iter().enumerate() {
let mut bucket = (*bucket).clone(); let mut bucket = (*bucket).clone();
let completed_contexts = completed_indexes_loop_arc.clone(); let completed_contexts = completed_indexes_loop_arc.clone();
@ -430,10 +453,23 @@ impl GameDownloadAgent {
let sender = self.sender.clone(); let sender = self.sender.clone();
let download_context = download_contexts let download_context = match download_contexts
.get(&bucket.version) .get(&bucket.version)
.ok_or(RemoteAccessError::CorruptedState) .ok_or(RemoteAccessError::CorruptedState)
.unwrap(); {
Ok(context) => context,
Err(e) => {
error!("Could not get download context with error {e}");
send!(
sender,
DownloadManagerSignal::Error(ApplicationDownloadError::DownloadError(
e
))
);
return;
}
};
scope.spawn(move |_| { scope.spawn(move |_| {
// 3 attempts // 3 attempts
@ -460,11 +496,12 @@ impl GameDownloadAgent {
ApplicationDownloadError::Communication(_) ApplicationDownloadError::Communication(_)
| ApplicationDownloadError::Checksum | ApplicationDownloadError::Checksum
| ApplicationDownloadError::Lock | ApplicationDownloadError::Lock
| ApplicationDownloadError::IoError(_)
); );
if i == RETRY_COUNT - 1 || !retry { if i == RETRY_COUNT - 1 || !retry {
warn!("retry logic failed, not re-attempting."); warn!("retry logic failed, not re-attempting.");
sender.send(DownloadManagerSignal::Error(e)).unwrap(); send!(sender, DownloadManagerSignal::Error(e));
return; return;
} }
} }
@ -477,7 +514,7 @@ impl GameDownloadAgent {
let newly_completed = completed_contexts.clone(); let newly_completed = completed_contexts.clone();
let completed_lock_len = { let completed_lock_len = {
let mut context_map_lock = self.context_map.lock().unwrap(); let mut context_map_lock = lock!(self.context_map);
for (_, item) in newly_completed.iter() { for (_, item) in newly_completed.iter() {
context_map_lock.insert(item.clone(), true); context_map_lock.insert(item.clone(), true);
} }
@ -485,7 +522,7 @@ impl GameDownloadAgent {
context_map_lock.values().filter(|x| **x).count() context_map_lock.values().filter(|x| **x).count()
}; };
let context_map_lock = self.context_map.lock().unwrap(); let context_map_lock = lock!(self.context_map);
let contexts = buckets let contexts = buckets
.iter() .iter()
.flat_map(|x| x.drops.iter().map(|e| e.checksum.clone())) .flat_map(|x| x.drops.iter().map(|e| e.checksum.clone()))
@ -534,7 +571,7 @@ impl GameDownloadAgent {
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> { pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
self.setup_validate(app_handle); self.setup_validate(app_handle);
let buckets = self.buckets.lock().unwrap(); let buckets = lock!(self.buckets);
let contexts: Vec<DropValidateContext> = buckets let contexts: Vec<DropValidateContext> = buckets
.clone() .clone()
.into_iter() .into_iter()
@ -546,7 +583,9 @@ impl GameDownloadAgent {
let pool = ThreadPoolBuilder::new() let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads) .num_threads(max_download_threads)
.build() .build()
.unwrap(); .unwrap_or_else(|_| {
panic!("failed to build thread pool with {max_download_threads} threads")
});
let invalid_chunks = Arc::new(boxcar::Vec::new()); let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| { pool.scope(|scope| {
@ -564,7 +603,7 @@ impl GameDownloadAgent {
} }
Err(e) => { Err(e) => {
error!("{e}"); error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap(); send!(sender, DownloadManagerSignal::Error(e));
} }
} }
}); });
@ -591,7 +630,7 @@ impl GameDownloadAgent {
// See docs on usage // See docs on usage
set_partially_installed( set_partially_installed(
&self.metadata(), &self.metadata(),
self.dropdata.base_path.to_str().unwrap().to_string(), self.dropdata.base_path.display().to_string(),
Some(app_handle), Some(app_handle),
); );
@ -601,12 +640,12 @@ impl GameDownloadAgent {
impl Downloadable for GameDownloadAgent { impl Downloadable for GameDownloadAgent {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> { fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Downloading; *lock!(self.status) = DownloadStatus::Downloading;
self.download(app_handle) self.download(app_handle)
} }
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> { fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating; *lock!(self.status) = DownloadStatus::Validating;
self.validate(app_handle) self.validate(app_handle)
} }
@ -626,17 +665,24 @@ impl Downloadable for GameDownloadAgent {
} }
} }
fn on_initialised(&self, _app_handle: &tauri::AppHandle) { fn on_queued(&self, app_handle: &tauri::AppHandle) {
*self.status.lock().unwrap() = DownloadStatus::Queued; *self.status.lock().unwrap() = DownloadStatus::Queued;
let mut db_lock = borrow_db_mut_checked();
let status = ApplicationTransientStatus::Queued {
version_name: self.version.clone(),
};
db_lock
.applications
.transient_statuses
.insert(self.metadata(), status.clone());
push_game_update(app_handle, &self.id, None, (None, Some(status)));
} }
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) { fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
*self.status.lock().unwrap() = DownloadStatus::Error; *lock!(self.status) = DownloadStatus::Error;
app_handle app_emit!(app_handle, "download_error", error.to_string());
.emit("download_error", error.to_string())
.unwrap();
error!("error while managing download: {error}"); error!("error while managing download: {error:?}");
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
handle handle
@ -653,27 +699,25 @@ impl Downloadable for GameDownloadAgent {
} }
fn on_complete(&self, app_handle: &tauri::AppHandle) { fn on_complete(&self, app_handle: &tauri::AppHandle) {
on_game_complete( match on_game_complete(
&self.metadata(), &self.metadata(),
self.dropdata.base_path.to_string_lossy().to_string(), self.dropdata.base_path.to_string_lossy().to_string(),
app_handle, app_handle,
) ) {
.unwrap(); Ok(_) => {}
Err(e) => {
error!("could not mark game as complete: {e}");
self.on_error(app_handle, &ApplicationDownloadError::DownloadError(e));
}
}
} }
fn on_cancelled(&self, app_handle: &tauri::AppHandle) { fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
info!("cancelled {}", self.id);
self.cancel(app_handle); self.cancel(app_handle);
/*
on_game_incomplete(
&self.metadata(),
self.dropdata.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
*/
} }
fn status(&self) -> DownloadStatus { fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone() lock!(self.status).clone()
} }
} }

View File

@ -9,7 +9,7 @@ use crate::games::downloads::manifest::{ChunkBody, DownloadBucket, DownloadConte
use crate::remote::auth::generate_authorization_header; use crate::remote::auth::generate_authorization_header;
use crate::remote::requests::generate_url; use crate::remote::requests::generate_url;
use crate::remote::utils::DROP_CLIENT_SYNC; use crate::remote::utils::DROP_CLIENT_SYNC;
use log::{info, warn}; use log::{debug, info, warn};
use md5::{Context, Digest}; use md5::{Context, Digest};
use reqwest::blocking::Response; use reqwest::blocking::Response;
@ -18,6 +18,7 @@ use std::io::Read;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
use std::{ use std::{
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::{self, BufWriter, Seek, SeekFrom, Write}, io::{self, BufWriter, Seek, SeekFrom, Write},
@ -25,6 +26,7 @@ use std::{
}; };
static MAX_PACKET_LENGTH: usize = 4096 * 4; static MAX_PACKET_LENGTH: usize = 4096 * 4;
static BUMP_SIZE: usize = 4096 * 16;
pub struct DropWriter<W: Write> { pub struct DropWriter<W: Write> {
hasher: Context, hasher: Context,
@ -79,6 +81,8 @@ pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
pub drops: Vec<DownloadDrop>, pub drops: Vec<DownloadDrop>,
pub destination: Vec<DropWriter<W>>, pub destination: Vec<DropWriter<W>>,
pub control_flag: &'a DownloadThreadControl, pub control_flag: &'a DownloadThreadControl,
#[allow(dead_code)]
progress: ProgressHandle,
} }
impl<'a> DropDownloadPipeline<'a, Response, File> { impl<'a> DropDownloadPipeline<'a, Response, File> {
@ -96,6 +100,7 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
.try_collect()?, .try_collect()?,
drops, drops,
control_flag, control_flag,
progress,
}) })
} }
@ -105,19 +110,29 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
let destination = self let destination = self
.destination .destination
.get_mut(index) .get_mut(index)
.ok_or(io::Error::other("no destination")) .ok_or(io::Error::other("no destination"))?;
.unwrap();
let mut remaining = drop.length; let mut remaining = drop.length;
if drop.start != 0 { if drop.start != 0 {
destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?; destination.seek(SeekFrom::Start(drop.start as u64))?;
} }
let mut last_bump = 0;
loop { loop {
let size = MAX_PACKET_LENGTH.min(remaining); let size = MAX_PACKET_LENGTH.min(remaining);
self.source.read_exact(&mut copy_buffer[0..size])?; let size = self.source.read(&mut copy_buffer[0..size]).inspect_err(|_| {
info!("got error from {}", drop.filename);
})?;
remaining -= size; remaining -= size;
last_bump += size;
destination.write_all(&copy_buffer[0..size])?; destination.write_all(&copy_buffer[0..size])?;
if last_bump > BUMP_SIZE {
last_bump -= BUMP_SIZE;
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
return Ok(false);
}
}
if remaining == 0 { if remaining == 0 {
break; break;
}; };
@ -131,6 +146,13 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
Ok(true) Ok(true)
} }
#[allow(dead_code)]
fn debug_skip_checksum(self) {
self.destination
.into_iter()
.for_each(|mut e| e.flush().unwrap());
}
fn finish(self) -> Result<Vec<Digest>, io::Error> { fn finish(self) -> Result<Vec<Digest>, io::Error> {
let checksums = self let checksums = self
.destination .destination
@ -153,6 +175,8 @@ pub fn download_game_bucket(
return Ok(false); return Ok(false);
} }
let start = Instant::now();
let header = generate_authorization_header(); let header = generate_authorization_header();
let url = generate_url(&["/api/v2/client/chunk"], &[]) let url = generate_url(&["/api/v2/client/chunk"], &[])
@ -190,25 +214,46 @@ pub fn download_game_bucket(
RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()), RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()),
))? ))?
.to_str() .to_str()
.unwrap(); .map_err(|e| {
ApplicationDownloadError::Communication(RemoteAccessError::UnparseableResponse(
e.to_string(),
))
})?;
for (i, raw_length) in lengths.split(",").enumerate() { for (i, raw_length) in lengths.split(",").enumerate() {
let length = raw_length.parse::<usize>().unwrap_or(0); let length = raw_length.parse::<usize>().unwrap_or(0);
let Some(drop) = bucket.drops.get(i) else { let Some(drop) = bucket.drops.get(i) else {
warn!( warn!("invalid number of Content-Lengths recieved: {i}, {lengths}");
return Err(ApplicationDownloadError::DownloadError(
RemoteAccessError::InvalidResponse(DropServerError {
status_code: 400,
status_message: format!(
"invalid number of Content-Lengths recieved: {i}, {lengths}" "invalid number of Content-Lengths recieved: {i}, {lengths}"
); ),
return Err(ApplicationDownloadError::DownloadError); }),
));
}; };
if drop.length != length { if drop.length != length {
warn!( warn!(
"for {}, expected {}, got {} ({})", "for {}, expected {}, got {} ({})",
drop.filename, drop.length, raw_length, length drop.filename, drop.length, raw_length, length
); );
return Err(ApplicationDownloadError::DownloadError); return Err(ApplicationDownloadError::DownloadError(
RemoteAccessError::InvalidResponse(DropServerError {
status_code: 400,
status_message: format!(
"for {}, expected {}, got {} ({})",
drop.filename, drop.length, raw_length, length
),
}),
));
} }
} }
let timestep = start.elapsed().as_millis();
debug!("took {}ms to start downloading", timestep);
let mut pipeline = let mut pipeline =
DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress) DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress)
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?; .map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;

View File

@ -5,6 +5,8 @@ use std::{
use log::error; use log::error;
use native_model::{Decode, Encode}; use native_model::{Decode, Encode};
use crate::lock;
pub type DropData = v1::DropData; pub type DropData = v1::DropData;
pub static DROP_DATA_PATH: &str = ".dropdata"; pub static DROP_DATA_PATH: &str = ".dropdata";
@ -49,7 +51,12 @@ impl DropData {
let mut s = Vec::new(); let mut s = Vec::new();
file.read_to_end(&mut s)?; file.read_to_end(&mut s)?;
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap()) native_model::rmp_serde_1_3::RmpSerde::decode(s).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to decode drop data: {e}"),
)
})
} }
pub fn write(&self) { pub fn write(&self) {
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) { let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
@ -71,12 +78,12 @@ impl DropData {
} }
} }
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) { pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
*self.contexts.lock().unwrap() = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect(); *lock!(self.contexts) = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();
} }
pub fn set_context(&self, context: String, state: bool) { pub fn set_context(&self, context: String, state: bool) {
self.contexts.lock().unwrap().entry(context).insert_entry(state); lock!(self.contexts).entry(context).insert_entry(state);
} }
pub fn get_contexts(&self) -> HashMap<String, bool> { pub fn get_contexts(&self) -> HashMap<String, bool> {
self.contexts.lock().unwrap().clone() lock!(self.contexts).clone()
} }
} }

View File

@ -36,14 +36,14 @@ pub fn validate_game_chunk(
if ctx.offset != 0 { if ctx.offset != 0 {
source source
.seek(SeekFrom::Start(ctx.offset.try_into().unwrap())) .seek(SeekFrom::Start(ctx.offset as u64))
.expect("Failed to seek to file offset"); .expect("Failed to seek to file offset");
} }
let mut hasher = md5::Context::new(); let mut hasher = md5::Context::new();
let completed = let completed =
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap(); validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress)?;
if !completed { if !completed {
return Ok(false); return Ok(false);
} }

View File

@ -8,6 +8,7 @@ use tauri::AppHandle;
use tauri::Emitter; use tauri::Emitter;
use crate::AppState; use crate::AppState;
use crate::app_emit;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::Database; use crate::database::models::data::Database;
use crate::database::models::data::{ use crate::database::models::data::{
@ -18,6 +19,7 @@ use crate::error::drop_server_error::DropServerError;
use crate::error::library_error::LibraryError; use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::lock;
use crate::remote::auth::generate_authorization_header; use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::cache_object_db; use crate::remote::cache::cache_object_db;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db}; use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
@ -80,7 +82,13 @@ pub struct StatsUpdateEvent {
pub async fn fetch_library_logic( pub async fn fetch_library_logic(
state: tauri::State<'_, Mutex<AppState<'_>>>, state: tauri::State<'_, Mutex<AppState<'_>>>,
hard_fresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> { ) -> Result<Vec<Game>, RemoteAccessError> {
let do_hard_refresh = hard_fresh.unwrap_or(false);
if !do_hard_refresh && let Ok(library) = get_cached_object("library") {
return Ok(library);
}
let client = DROP_CLIENT_ASYNC.clone(); let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/user/library"], &[])?; let response = generate_url(&["/api/v1/client/user/library"], &[])?;
let response = client let response = client
@ -100,7 +108,7 @@ pub async fn fetch_library_logic(
let mut games: Vec<Game> = response.json().await?; let mut games: Vec<Game> = response.json().await?;
let mut handle = state.lock().unwrap(); let mut handle = lock!(state);
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
@ -142,6 +150,7 @@ pub async fn fetch_library_logic(
} }
pub async fn fetch_library_logic_offline( pub async fn fetch_library_logic_offline(
_state: tauri::State<'_, Mutex<AppState<'_>>>, _state: tauri::State<'_, Mutex<AppState<'_>>>,
_hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> { ) -> Result<Vec<Game>, RemoteAccessError> {
let mut games: Vec<Game> = get_cached_object("library")?; let mut games: Vec<Game> = get_cached_object("library")?;
@ -165,7 +174,7 @@ pub async fn fetch_game_logic(
state: tauri::State<'_, Mutex<AppState<'_>>>, state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let version = { let version = {
let state_handle = state.lock().unwrap(); let state_handle = lock!(state);
let db_lock = borrow_db_checked(); let db_lock = borrow_db_checked();
@ -215,14 +224,14 @@ pub async fn fetch_game_logic(
return Err(RemoteAccessError::GameNotFound(id)); return Err(RemoteAccessError::GameNotFound(id));
} }
if response.status() != 200 { if response.status() != 200 {
let err = response.json().await.unwrap(); let err = response.json().await?;
warn!("{err:?}"); warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err)); return Err(RemoteAccessError::InvalidResponse(err));
} }
let game: Game = response.json().await?; let game: Game = response.json().await?;
let mut state_handle = state.lock().unwrap(); let mut state_handle = lock!(state);
state_handle.games.insert(id.clone(), game.clone()); state_handle.games.insert(id.clone(), game.clone());
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
@ -290,22 +299,18 @@ pub async fn fetch_game_version_options_logic(
.await?; .await?;
if response.status() != 200 { if response.status() != 200 {
let err = response.json().await.unwrap(); let err = response.json().await?;
warn!("{err:?}"); warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err)); return Err(RemoteAccessError::InvalidResponse(err));
} }
let data: Vec<GameVersion> = response.json().await?; let data: Vec<GameVersion> = response.json().await?;
let state_lock = state.lock().unwrap(); let state_lock = lock!(state);
let process_manager_lock = state_lock.process_manager.lock().unwrap(); let process_manager_lock = lock!(state_lock.process_manager);
let data: Vec<GameVersion> = data let data: Vec<GameVersion> = data
.into_iter() .into_iter()
.filter(|v| { .filter(|v| process_manager_lock.valid_platform(&v.platform, &state_lock))
process_manager_lock
.valid_platform(&v.platform, &state_lock)
.unwrap()
})
.collect(); .collect();
drop(process_manager_lock); drop(process_manager_lock);
drop(state_lock); drop(state_lock);
@ -372,11 +377,13 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
); );
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
if previous_state.is_none() {
let previous_state = if let Some(state) = previous_state {
state
} else {
warn!("uninstall job doesn't have previous state, failing silently"); warn!("uninstall job doesn't have previous state, failing silently");
return; return;
} };
let previous_state = previous_state.unwrap();
if let Some((_, install_dir)) = match previous_state { if let Some((_, install_dir)) = match previous_state {
GameDownloadStatus::Installed { GameDownloadStatus::Installed {
@ -425,7 +432,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
); );
debug!("uninstalled game id {}", &meta.id); debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap(); app_emit!(app_handle, "update_library", ());
} }
}); });
} else { } else {
@ -498,17 +505,15 @@ pub fn on_game_complete(
.game_statuses .game_statuses
.insert(meta.id.clone(), status.clone()); .insert(meta.id.clone(), status.clone());
drop(db_handle); drop(db_handle);
app_emit!(
app_handle app_handle,
.emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
GameUpdateEvent { GameUpdateEvent {
game_id: meta.id.clone(), game_id: meta.id.clone(),
status: (Some(status), None), status: (Some(status), None),
version: Some(game_version), version: Some(game_version),
}, }
) );
.unwrap();
Ok(()) Ok(())
} }
@ -521,20 +526,20 @@ pub fn push_game_update(
) { ) {
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) = if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
&status.0 &status.0
&& version.is_none() { && version.is_none()
{
panic!("pushed game for installed game that doesn't have version information"); panic!("pushed game for installed game that doesn't have version information");
} }
app_handle app_emit!(
.emit( app_handle,
&format!("update_game/{game_id}"), &format!("update_game/{game_id}"),
GameUpdateEvent { GameUpdateEvent {
game_id: game_id.clone(), game_id: game_id.clone(),
status, status,
version, version,
}, }
) );
.unwrap();
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -556,7 +561,7 @@ pub fn update_game_configuration(
.ok_or(LibraryError::MetaNotFound(game_id))?; .ok_or(LibraryError::MetaNotFound(game_id))?;
let id = installed_version.id.clone(); let id = installed_version.id.clone();
let version = installed_version.version.clone().unwrap(); let version = installed_version.version.clone().ok_or(LibraryError::VersionNotFound(id.clone()))?;
let mut existing_configuration = handle let mut existing_configuration = handle
.applications .applications

View File

@ -1,4 +1,6 @@
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus}; use crate::database::models::data::{
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus,
};
pub type GameStatusWithTransient = ( pub type GameStatusWithTransient = (
Option<GameDownloadStatus>, Option<GameDownloadStatus>,
@ -8,10 +10,16 @@ pub struct GameStatusManager {}
impl GameStatusManager { impl GameStatusManager {
pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient { pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient {
let online_state = match database.applications.installed_game_version.get(game_id) { let online_state = database
Some(meta) => database.applications.transient_statuses.get(meta).cloned(), .applications
None => None, .transient_statuses
}; .get(&DownloadableMetadata {
id: game_id.to_string(),
download_type: DownloadType::Game,
version: None,
})
.cloned();
let offline_state = database.applications.game_statuses.get(game_id).cloned(); let offline_state = database.applications.game_statuses.get(game_id).cloned();
if online_state.is_some() { if online_state.is_some() {

View File

@ -4,7 +4,6 @@
#![feature(duration_millis_float)] #![feature(duration_millis_float)]
#![feature(iterator_try_collect)] #![feature(iterator_try_collect)]
#![deny(clippy::all)] #![deny(clippy::all)]
#![deny(clippy::unwrap_used)]
mod database; mod database;
mod games; mod games;
@ -20,6 +19,8 @@ use crate::database::scan::scan_install_dirs;
use crate::process::commands::open_process_logs; use crate::process::commands::open_process_logs;
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE; use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
use crate::remote::commands::auth_initiate_code; use crate::remote::commands::auth_initiate_code;
use crate::remote::fetch_object::fetch_object_wrapper;
use crate::remote::server_proto::handle_server_proto_wrapper;
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download}; use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use client::commands::fetch_state; use client::commands::fetch_state;
@ -47,7 +48,7 @@ use games::commands::{
}; };
use games::downloads::commands::download_game; use games::downloads::commands::download_game;
use games::library::{Game, update_game_configuration}; use games::library::{Game, update_game_configuration};
use log::{LevelFilter, debug, info, warn, error}; use log::{LevelFilter, debug, info, warn};
use log4rs::Config; use log4rs::Config;
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
@ -60,8 +61,7 @@ use remote::commands::{
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect, auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect,
sign_out, use_remote, sign_out, use_remote,
}; };
use remote::fetch_object::fetch_object; use remote::server_proto::handle_server_proto_offline_wrapper;
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@ -462,7 +462,7 @@ pub fn run() {
}) })
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| { .register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
fetch_object(request, responder).await; fetch_object_wrapper(request, responder).await;
}); });
}) })
.register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| { .register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| {
@ -473,8 +473,8 @@ pub fn run() {
offline!( offline!(
state, state,
handle_server_proto, handle_server_proto_wrapper,
handle_server_proto_offline, handle_server_proto_offline_wrapper,
request, request,
responder responder
) )

View File

@ -1,14 +1,14 @@
use std::sync::Mutex; use std::sync::Mutex;
use crate::{error::process_error::ProcessError, AppState}; use crate::{error::process_error::ProcessError, lock, AppState};
#[tauri::command] #[tauri::command]
pub fn launch_game( pub fn launch_game(
id: String, id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> { ) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap(); let state_lock = lock!(state);
let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); let mut process_manager_lock = lock!(state_lock.process_manager);
//let meta = DownloadableMetadata { //let meta = DownloadableMetadata {
// id, // id,
@ -32,8 +32,8 @@ pub fn kill_game(
game_id: String, game_id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> { ) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap(); let state_lock = lock!(state);
let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); let mut process_manager_lock = lock!(state_lock.process_manager);
process_manager_lock process_manager_lock
.kill_game(game_id) .kill_game(game_id)
.map_err(ProcessError::IOError) .map_err(ProcessError::IOError)
@ -44,7 +44,7 @@ pub fn open_process_logs(
game_id: String, game_id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> { ) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap(); let state_lock = lock!(state);
let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); let mut process_manager_lock = lock!(state_lock.process_manager);
process_manager_lock.open_process_logs(game_id) process_manager_lock.open_process_logs(game_id)
} }

View File

@ -10,6 +10,7 @@ use log::{debug, info};
use crate::{ use crate::{
AppState, AppState,
database::models::data::{Database, DownloadableMetadata, GameVersion}, database::models::data::{Database, DownloadableMetadata, GameVersion},
error::process_error::ProcessError,
process::process_manager::{Platform, ProcessHandler}, process::process_manager::{Platform, ProcessHandler},
}; };
@ -22,8 +23,8 @@ impl ProcessHandler for NativeGameLauncher {
args: Vec<String>, args: Vec<String>,
_game_version: &GameVersion, _game_version: &GameVersion,
_current_dir: &str, _current_dir: &str,
) -> String { ) -> Result<String, ProcessError> {
format!("\"{}\" {}", launch_command, args.join(" ")) Ok(format!("\"{}\" {}", launch_command, args.join(" ")))
} }
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool { fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
@ -65,7 +66,7 @@ impl ProcessHandler for UMULauncher {
args: Vec<String>, args: Vec<String>,
game_version: &GameVersion, game_version: &GameVersion,
_current_dir: &str, _current_dir: &str,
) -> String { ) -> Result<String, ProcessError> {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override); debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override { let game_id = match &game_version.umu_id_override {
Some(game_override) => { Some(game_override) => {
@ -77,12 +78,12 @@ impl ProcessHandler for UMULauncher {
} }
None => game_version.game_id.clone(), None => game_version.game_id.clone(),
}; };
format!( Ok(format!(
"GAMEID={game_id} {umu:?} \"{launch}\" {args}", "GAMEID={game_id} {umu:?} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE.as_ref().unwrap(), umu = UMU_LAUNCHER_EXECUTABLE.as_ref().expect("Failed to get UMU_LAUNCHER_EXECUTABLE as ref"),
launch = launch_command, launch = launch_command,
args = args.join(" ") args = args.join(" ")
) ))
} }
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool { fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
@ -102,7 +103,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
args: Vec<String>, args: Vec<String>,
game_version: &GameVersion, game_version: &GameVersion,
current_dir: &str, current_dir: &str,
) -> String { ) -> Result<String, ProcessError> {
let umu_launcher = UMULauncher {}; let umu_launcher = UMULauncher {};
let umu_string = umu_launcher.create_launch_process( let umu_string = umu_launcher.create_launch_process(
meta, meta,
@ -110,15 +111,18 @@ impl ProcessHandler for AsahiMuvmLauncher {
args, args,
game_version, game_version,
current_dir, current_dir,
); )?;
let mut args_cmd = umu_string let mut args_cmd = umu_string
.split("umu-run") .split("umu-run")
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.into_iter(); .into_iter();
let args = args_cmd.next().unwrap().trim(); let args = args_cmd
let cmd = format!("umu-run{}", args_cmd.next().unwrap()); .next()
.ok_or(ProcessError::InvalidArguments(umu_string.clone()))?
.trim();
let cmd = format!("umu-run{}", args_cmd.next().ok_or(ProcessError::InvalidArguments(umu_string.clone()))?);
format!("{args} muvm -- {cmd}") Ok(format!("{args} muvm -- {cmd}"))
} }
#[allow(unreachable_code)] #[allow(unreachable_code)]

View File

@ -19,7 +19,7 @@ use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
use crate::{ use crate::{
AppState, DB, AppState,
database::{ database::{
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked}, db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
models::data::{ models::data::{
@ -33,6 +33,7 @@ use crate::{
format::DropFormatArgs, format::DropFormatArgs,
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher}, process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
}, },
lock,
}; };
pub struct RunningProcess { pub struct RunningProcess {
@ -118,7 +119,7 @@ impl ProcessManager<'_> {
let dir = self.get_log_dir(game_id); let dir = self.get_log_dir(game_id);
self.app_handle self.app_handle
.opener() .opener()
.open_path(dir.to_str().unwrap(), None::<&str>) .open_path(dir.display().to_string(), None::<&str>)
.map_err(ProcessError::OpenerError)?; .map_err(ProcessError::OpenerError)?;
Ok(()) Ok(())
} }
@ -133,7 +134,13 @@ impl ProcessManager<'_> {
debug!("process for {:?} exited with {:?}", &game_id, result); debug!("process for {:?} exited with {:?}", &game_id, result);
let process = self.processes.remove(&game_id).unwrap(); let process = match self.processes.remove(&game_id) {
Some(process) => process,
None => {
info!("Attempted to stop process {game_id} which didn't exist");
return;
}
};
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
let meta = db_handle let meta = db_handle
@ -141,7 +148,7 @@ impl ProcessManager<'_> {
.installed_game_version .installed_game_version
.get(&game_id) .get(&game_id)
.cloned() .cloned()
.unwrap(); .unwrap_or_else(|| panic!("Could not get installed version of {}", &game_id));
db_handle.applications.transient_statuses.remove(&meta); db_handle.applications.transient_statuses.remove(&meta);
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned(); let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
@ -166,20 +173,17 @@ impl ProcessManager<'_> {
// Or if the status isn't 0 // Or if the status isn't 0
// Or if it's an error // Or if it's an error
if !process.manually_killed if !process.manually_killed
&& (elapsed.as_secs() <= 2 || result.is_err() || !result.unwrap().success()) && (elapsed.as_secs() <= 2 || result.map_or(true, |r| !r.success()))
{ {
warn!("drop detected that the game {game_id} may have failed to launch properly"); 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 _ = self.app_handle.emit("launch_external_error", &game_id);
} }
// This is too many unwraps for me to be comfortable let version_data = match db_handle.applications.game_versions.get(&game_id) {
let version_data = db_handle // This unwrap here should be resolved by just making the hashmap accept an option rather than just a String
.applications Some(res) => res.get(&meta.version.unwrap()).expect("Failed to get game version from installed game versions. Is the database corrupted?"),
.game_versions None => todo!(),
.get(&game_id) };
.unwrap()
.get(&meta.version.unwrap())
.unwrap();
let status = GameStatusManager::fetch_state(&game_id, &db_handle); let status = GameStatusManager::fetch_state(&game_id, &db_handle);
@ -210,10 +214,10 @@ impl ProcessManager<'_> {
.1) .1)
} }
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> { pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> bool {
let db_lock = borrow_db_checked(); let db_lock = borrow_db_checked();
let process_handler = self.fetch_process_handler(&db_lock, state, platform); let process_handler = self.fetch_process_handler(&db_lock, state, platform);
Ok(process_handler.is_ok()) process_handler.is_ok()
} }
pub fn launch_process( pub fn launch_process(
@ -225,9 +229,7 @@ impl ProcessManager<'_> {
return Err(ProcessError::AlreadyRunning); return Err(ProcessError::AlreadyRunning);
} }
let version = match DB let version = match borrow_db_checked()
.borrow_data()
.unwrap()
.applications .applications
.game_statuses .game_statuses
.get(&game_id) .get(&game_id)
@ -266,7 +268,7 @@ impl ProcessManager<'_> {
debug!( debug!(
"Launching process {:?} with version {:?}", "Launching process {:?} with version {:?}",
&game_id, &game_id,
db_lock.applications.game_versions.get(&game_id).unwrap() db_lock.applications.game_versions.get(&game_id)
); );
let game_version = db_lock let game_version = db_lock
@ -322,8 +324,9 @@ impl ProcessManager<'_> {
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"), GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
}; };
#[allow(clippy::unwrap_used)]
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 = launch.display().to_string();
let launch_string = process_handler.create_launch_process( let launch_string = process_handler.create_launch_process(
&meta, &meta,
@ -331,7 +334,7 @@ impl ProcessManager<'_> {
args.clone(), args.clone(),
game_version, game_version,
install_dir, install_dir,
); )?;
let format_args = DropFormatArgs::new( let format_args = DropFormatArgs::new(
launch_string, launch_string,
@ -392,9 +395,12 @@ impl ProcessManager<'_> {
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait(); let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>(); let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
let app_state_handle = app_state.lock().unwrap(); let app_state_handle = lock!(app_state);
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap(); let mut process_manager_handle = app_state_handle
.process_manager
.lock()
.expect("Failed to lock onto process manager");
process_manager_handle.on_process_finish(wait_thread_game_id.id, result); process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
// As everything goes out of scope, they should get dropped // As everything goes out of scope, they should get dropped
@ -468,7 +474,7 @@ pub trait ProcessHandler: Send + 'static {
args: Vec<String>, args: Vec<String>,
game_version: &GameVersion, game_version: &GameVersion,
current_dir: &str, current_dir: &str,
) -> String; ) -> Result<String, ProcessError>;
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool; fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
} }

View File

@ -9,10 +9,10 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::{ app_emit, database::{
db::{borrow_db_checked, borrow_db_mut_checked}, db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth, models::data::DatabaseAuth,
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::{requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User }, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, lock, remote::{cache::clear_cached_object, requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User
}; };
use super::{ use super::{
@ -51,12 +51,13 @@ struct HandshakeResponse {
pub fn generate_authorization_header() -> String { pub fn generate_authorization_header() -> String {
let certs = { let certs = {
let db = borrow_db_checked(); let db = borrow_db_checked();
db.auth.clone().unwrap() db.auth.clone().expect("Authorisation not initialised")
}; };
let nonce = Utc::now().timestamp_millis().to_string(); let nonce = Utc::now().timestamp_millis().to_string();
let signature = sign_nonce(certs.private, nonce.clone()).unwrap(); let signature =
sign_nonce(certs.private, nonce.clone()).expect("Failed to generate authorisation header");
format!("Nonce {} {} {}", certs.client_id, nonce, signature) format!("Nonce {} {} {}", certs.client_id, nonce, signature)
} }
@ -83,7 +84,7 @@ pub async fn fetch_user() -> Result<User, RemoteAccessError> {
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> { async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
let path_chunks: Vec<&str> = path.split('/').collect(); let path_chunks: Vec<&str> = path.split('/').collect();
if path_chunks.len() != 3 { if path_chunks.len() != 3 {
app.emit("auth/failed", ()).unwrap(); app_emit!(app, "auth/failed", ());
return Err(RemoteAccessError::HandshakeFailed( return Err(RemoteAccessError::HandshakeFailed(
"failed to parse token".to_string(), "failed to parse token".to_string(),
)); ));
@ -94,11 +95,15 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
Url::parse(handle.base_url.as_str())? Url::parse(handle.base_url.as_str())?
}; };
let client_id = path_chunks.get(1).unwrap(); let client_id = path_chunks
let token = path_chunks.get(2).unwrap(); .get(1)
.expect("Failed to get client id from path chunks");
let token = path_chunks
.get(2)
.expect("Failed to get token from path chunks");
let body = HandshakeRequestBody { let body = HandshakeRequestBody {
client_id: (*client_id).to_string(), client_id: (client_id).to_string(),
token: (*token).to_string(), token: (token).to_string(),
}; };
let endpoint = base_url.join("/api/v1/client/auth/handshake")?; let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
@ -116,37 +121,34 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
private: response_struct.private, private: response_struct.private,
cert: response_struct.certificate, cert: response_struct.certificate,
client_id: response_struct.id, client_id: response_struct.id,
web_token: None, // gets created later web_token: None,
}); });
} }
let web_token = { let web_token = {
let header = generate_authorization_header(); let header = generate_authorization_header();
let token = client let token = client
.post(base_url.join("/api/v1/client/user/webtoken").unwrap()) .post(base_url.join("/api/v1/client/user/webtoken")?)
.header("Authorization", header) .header("Authorization", header)
.send() .send()
.await .await?;
.unwrap();
token.text().await.unwrap() token.text().await?
}; };
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
let mut_auth = handle.auth.as_mut().unwrap(); handle.auth.as_mut().unwrap().web_token = Some(web_token);
mut_auth.web_token = Some(web_token);
Ok(()) Ok(())
} }
pub async fn recieve_handshake(app: AppHandle, path: String) { pub async fn recieve_handshake(app: AppHandle, path: String) {
// Tell the app we're processing // Tell the app we're processing
app.emit("auth/processing", ()).unwrap(); app_emit!(app, "auth/processing", ());
let handshake_result = recieve_handshake_logic(&app, path).await; let handshake_result = recieve_handshake_logic(&app, path).await;
if let Err(e) = handshake_result { if let Err(e) = handshake_result {
warn!("error with authentication: {e}"); warn!("error with authentication: {e}");
app.emit("auth/failed", e.to_string()).unwrap(); app_emit!(app, "auth/failed", e.to_string());
return; return;
} }
@ -154,14 +156,17 @@ pub async fn recieve_handshake(app: AppHandle, path: String) {
let (app_status, user) = setup().await; let (app_status, user) = setup().await;
let mut state_lock = app_state.lock().unwrap(); let mut state_lock = lock!(app_state);
state_lock.status = app_status; state_lock.status = app_status;
state_lock.user = user; state_lock.user = user;
let _ = clear_cached_object("collections");
let _ = clear_cached_object("library");
drop(state_lock); drop(state_lock);
app.emit("auth/finished", ()).unwrap(); app_emit!(app, "auth/finished", ());
} }
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> { pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
@ -174,7 +179,7 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
let endpoint = base_url.join("/api/v1/client/auth/initiate")?; let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
let body = InitiateRequestBody { let body = InitiateRequestBody {
name: format!("{} (Desktop)", hostname.into_string().unwrap()), name: format!("{} (Desktop)", hostname.display()),
platform: env::consts::OS.to_string(), platform: env::consts::OS.to_string(),
capabilities: HashMap::from([ capabilities: HashMap::from([
("peerAPI".to_owned(), CapabilityConfiguration {}), ("peerAPI".to_owned(), CapabilityConfiguration {}),
@ -208,12 +213,14 @@ pub async fn setup() -> (AppStatus, Option<User>) {
let user_result = match fetch_user().await { let user_result = match fetch_user().await {
Ok(data) => data, Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => { Err(RemoteAccessError::FetchError(_)) => {
let user = get_cached_object::<User>("user").unwrap(); let user = get_cached_object::<User>("user").ok();
return (AppStatus::Offline, Some(user)); return (AppStatus::Offline, user);
} }
Err(_) => return (AppStatus::SignedInNeedsReauth, None), Err(_) => return (AppStatus::SignedInNeedsReauth, None),
}; };
cache_object("user", &user_result).unwrap(); if let Err(e) = cache_object("user", &user_result) {
warn!("Could not cache user object with error {e}");
}
return (AppStatus::SignedIn, Some(user_result)); return (AppStatus::SignedIn, Some(user_result));
} }

View File

@ -7,16 +7,16 @@ use std::{
use crate::{ use crate::{
database::{db::borrow_db_checked, models::data::Database}, database::{db::borrow_db_checked, models::data::Database},
error::remote_access_error::RemoteAccessError, error::{cache_error::CacheError, remote_access_error::RemoteAccessError},
}; };
use bitcode::{Decode, DecodeOwned, Encode}; use bitcode::{Decode, DecodeOwned, Encode};
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder}; use http::{header::{CONTENT_TYPE}, response::Builder as ResponseBuilder, Response};
#[macro_export] #[macro_export]
macro_rules! offline { macro_rules! offline {
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => { ($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline { async move { if $crate::borrow_db_checked().settings.force_offline || $crate::lock!($var).status == $crate::AppStatus::Offline {
$func2( $( $arg ), *).await $func2( $( $arg ), *).await
} else { } else {
$func1( $( $arg ), *).await $func1( $( $arg ), *).await
@ -50,6 +50,12 @@ fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
Ok(file) Ok(file)
} }
fn delete_sync(base: &Path, key: &str) -> io::Result<()> {
let cache_path = get_cache_path(base, key);
std::fs::remove_file(cache_path)?;
Ok(())
}
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> { pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
cache_object_db(key, data, &borrow_db_checked()) cache_object_db(key, data, &borrow_db_checked())
} }
@ -73,6 +79,17 @@ pub fn get_cached_object_db<D: DecodeOwned>(
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?; bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
Ok(data) Ok(data)
} }
pub fn clear_cached_object(key: &str) -> Result<(), RemoteAccessError> {
clear_cached_object_db(key, &borrow_db_checked())
}
pub fn clear_cached_object_db(
key: &str,
db: &Database,
) -> Result<(), RemoteAccessError> {
delete_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
Ok(())
}
#[derive(Encode, Decode)] #[derive(Encode, Decode)]
pub struct ObjectCache { pub struct ObjectCache {
content_type: String, content_type: String,
@ -87,30 +104,36 @@ impl ObjectCache {
} }
} }
impl From<Response<Vec<u8>>> for ObjectCache { impl TryFrom<Response<Vec<u8>>> for ObjectCache {
fn from(value: Response<Vec<u8>>) -> Self { type Error = CacheError;
ObjectCache {
fn try_from(value: Response<Vec<u8>>) -> Result<Self, Self::Error> {
Ok(ObjectCache {
content_type: value content_type: value
.headers() .headers()
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
.unwrap() .ok_or(CacheError::HeaderNotFound(CONTENT_TYPE))?
.to_str() .to_str()
.unwrap() .map_err(CacheError::ParseError)?
.to_owned(), .to_owned(),
body: value.body().clone(), body: value.body().clone(),
expiry: get_sys_time_in_secs() + 60 * 60 * 24, expiry: get_sys_time_in_secs() + 60 * 60 * 24,
})
} }
} }
} impl TryFrom<ObjectCache> for Response<Vec<u8>> {
impl From<ObjectCache> for Response<Vec<u8>> { type Error = CacheError;
fn from(value: ObjectCache) -> Self { fn try_from(value: ObjectCache) -> Result<Self, Self::Error> {
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type); let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type);
resp_builder.body(value.body).unwrap() resp_builder.body(value.body).map_err(CacheError::ConstructionError)
} }
} }
impl From<&ObjectCache> for Response<Vec<u8>> { impl TryFrom<&ObjectCache> for Response<Vec<u8>> {
fn from(value: &ObjectCache) -> Self { type Error = CacheError;
fn try_from(value: &ObjectCache) -> Result<Self, Self::Error> {
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone()); let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
resp_builder.body(value.body.clone()).unwrap() resp_builder.body(value.body.clone()).map_err(CacheError::ConstructionError)
} }
} }

View File

@ -8,14 +8,16 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
AppState, AppStatus, AppState, AppStatus, app_emit,
database::db::{borrow_db_checked, borrow_db_mut_checked}, database::db::{borrow_db_checked, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
lock,
remote::{ remote::{
auth::generate_authorization_header, auth::generate_authorization_header,
requests::generate_url, requests::generate_url,
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT}, utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
}, },
utils::webbrowser_open::webbrowser_open,
}; };
use super::{ use super::{
@ -40,7 +42,7 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)? Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
}; };
let url = base_url.join(&path).unwrap(); let url = base_url.join(&path)?;
Ok(url.to_string()) Ok(url.to_string())
} }
@ -77,20 +79,20 @@ pub fn sign_out(app: AppHandle) {
// Update app state // Update app state
{ {
let app_state = app.state::<Mutex<AppState>>(); let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap(); let mut app_state_handle = lock!(app_state);
app_state_handle.status = AppStatus::SignedOut; app_state_handle.status = AppStatus::SignedOut;
app_state_handle.user = None; app_state_handle.user = None;
} }
// Emit event for frontend // Emit event for frontend
app.emit("auth/signedout", ()).unwrap(); app_emit!(app, "auth/signedout", ());
} }
#[tauri::command] #[tauri::command]
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> { pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> {
let (app_status, user) = setup().await; let (app_status, user) = setup().await;
let mut guard = state.lock().unwrap(); let mut guard = lock!(state);
guard.status = app_status; guard.status = app_status;
guard.user = user; guard.user = user;
drop(guard); drop(guard);
@ -109,7 +111,7 @@ pub fn auth_initiate() -> Result<(), RemoteAccessError> {
let complete_redir_url = base_url.join(&redir_url)?; let complete_redir_url = base_url.join(&redir_url)?;
debug!("opening web browser to continue authentication"); debug!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap(); webbrowser_open(complete_redir_url.as_ref());
Ok(()) Ok(())
} }
@ -124,7 +126,7 @@ struct CodeWebsocketResponse {
pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> { pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
let base_url = { let base_url = {
let db_lock = borrow_db_checked(); let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())? Url::parse(&db_lock.base_url.clone())?.clone()
}; };
let code = auth_initiate_logic("code".to_string())?; let code = auth_initiate_logic("code".to_string())?;
@ -151,14 +153,13 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
match response.response_type.as_str() { match response.response_type.as_str() {
"token" => { "token" => {
let recieve_app = app.clone(); let recieve_app = app.clone();
manual_recieve_handshake(recieve_app, response.value).await.unwrap(); manual_recieve_handshake(recieve_app, response.value).await;
return Ok(()); return Ok(());
} }
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)), _ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
} }
} }
} }
Err(RemoteAccessError::HandshakeFailed( Err(RemoteAccessError::HandshakeFailed(
"Failed to connect to websocket".to_string(), "Failed to connect to websocket".to_string(),
)) ))
@ -167,7 +168,7 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
let result = load().await; let result = load().await;
if let Err(err) = result { if let Err(err) = result {
warn!("{err}"); warn!("{err}");
app.emit("auth/failed", err.to_string()).unwrap(); app_emit!(app, "auth/failed", err.to_string());
} }
}); });
@ -175,8 +176,6 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
} }
#[tauri::command] #[tauri::command]
pub async fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), ()> { pub async fn manual_recieve_handshake(app: AppHandle, token: String) {
recieve_handshake(app, format!("handshake/{token}")).await; recieve_handshake(app, format!("handshake/{token}")).await;
Ok(())
} }

View File

@ -1,15 +1,26 @@
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder}; use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
use log::warn; use log::{debug, warn};
use tauri::UriSchemeResponder; use tauri::UriSchemeResponder;
use crate::{database::db::DatabaseImpls, remote::utils::DROP_CLIENT_ASYNC, DB}; use crate::{database::db::DatabaseImpls, error::cache_error::CacheError, remote::utils::DROP_CLIENT_ASYNC, DB};
use super::{ use super::{
auth::generate_authorization_header, auth::generate_authorization_header,
cache::{ObjectCache, cache_object, get_cached_object}, cache::{ObjectCache, cache_object, get_cached_object},
}; };
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) { pub async fn fetch_object_wrapper(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
match fetch_object(request).await {
Ok(r) => responder.respond(r),
Err(e) => {
warn!("Cache error: {e}");
responder.respond(Response::builder().status(500).body(Vec::new()).expect("Failed to build error response"));
}
};
}
pub async fn fetch_object(request: http::Request<Vec<u8>>) -> Result<Response<Vec<u8>>, CacheError>
{
// Drop leading / // Drop leading /
let object_id = &request.uri().path()[1..]; let object_id = &request.uri().path()[1..];
@ -17,8 +28,7 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
if let Ok(cache_result) = &cache_result if let Ok(cache_result) = &cache_result
&& !cache_result.has_expired() && !cache_result.has_expired()
{ {
responder.respond(cache_result.into()); return cache_result.try_into();
return;
} }
let header = generate_authorization_header(); let header = generate_authorization_header();
@ -26,26 +36,40 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url()); let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
let response = client.get(url).header("Authorization", header).send().await; let response = client.get(url).header("Authorization", header).send().await;
if response.is_err() { match response {
match cache_result { Ok(r) => {
Ok(cache_result) => responder.respond(cache_result.into()),
Err(e) => {
warn!("{e}");
}
}
return;
}
let response = response.unwrap();
let resp_builder = ResponseBuilder::new().header( let resp_builder = ResponseBuilder::new().header(
CONTENT_TYPE, CONTENT_TYPE,
response.headers().get("Content-Type").unwrap(), r.headers()
.get("Content-Type")
.expect("Failed get Content-Type header"),
); );
let data = Vec::from(response.bytes().await.unwrap()); let data = match r.bytes().await {
let resp = resp_builder.body(data).unwrap(); Ok(data) => Vec::from(data),
if cache_result.is_err() || cache_result.unwrap().has_expired() { Err(e) => {
cache_object::<ObjectCache>(object_id, &resp.clone().into()).unwrap(); warn!(
"Could not get data from cache object {object_id} with error {e}",
);
Vec::new()
}
};
let resp = resp_builder.body(data).expect("Failed to build object cache response body");
if cache_result.map_or(true, |x| x.has_expired()) {
cache_object::<ObjectCache>(object_id, &resp.clone().try_into()?)
.expect("Failed to create cached object");
} }
responder.respond(resp); Ok(resp)
}
Err(e) => {
debug!("Object fetch failed with error {e}. Attempting to download from cache");
match cache_result {
Ok(cache_result) => cache_result.try_into(),
Err(e) => {
warn!("{e}");
Err(CacheError::Remote(e))
}
}
}
}
} }

View File

@ -1,57 +1,91 @@
use std::str::FromStr; use std::str::FromStr;
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri}; use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
use log::{error, warn};
use tauri::UriSchemeResponder; use tauri::UriSchemeResponder;
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC}; use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC, utils::webbrowser_open::webbrowser_open};
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) { pub async fn handle_server_proto_offline_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
let four_oh_four = Response::builder() responder.respond(match handle_server_proto_offline(request).await {
.status(StatusCode::NOT_FOUND) Ok(res) => res,
.body(Vec::new()) Err(_) => unreachable!()
.unwrap(); });
responder.respond(four_oh_four);
} }
pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) { pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode>{
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Vec::new())
.expect("Failed to build error response for proto offline"))
}
pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
match handle_server_proto(request).await {
Ok(r) => responder.respond(r),
Err(e) => {
warn!("Cache error: {e}");
responder.respond(Response::builder().status(e).body(Vec::new()).expect("Failed to build error response"));
}
}
}
async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode> {
let db_handle = borrow_db_checked(); let db_handle = borrow_db_checked();
let web_token = match &db_handle.auth.as_ref().unwrap().web_token { let auth = match db_handle.auth.as_ref() {
Some(e) => e, Some(auth) => auth,
None => return, None => {
error!("Could not find auth in database");
return Err(StatusCode::UNAUTHORIZED)
}
}; };
let remote_uri = db_handle.base_url.parse::<Uri>().unwrap(); let web_token = match &auth.web_token {
Some(token) => token,
None => return Err(StatusCode::UNAUTHORIZED),
};
let remote_uri = db_handle.base_url.parse::<Uri>().expect("Failed to parse base url");
let path = request.uri().path(); let path = request.uri().path();
let mut new_uri = request.uri().clone().into_parts(); let mut new_uri = request.uri().clone().into_parts();
new_uri.path_and_query = new_uri.path_and_query =
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).unwrap()); Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).expect("Failed to parse request path in proto"));
new_uri.authority = remote_uri.authority().cloned(); new_uri.authority = remote_uri.authority().cloned();
new_uri.scheme = remote_uri.scheme().cloned(); new_uri.scheme = remote_uri.scheme().cloned();
let new_uri = Uri::from_parts(new_uri).unwrap(); let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"]; let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) { if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
webbrowser::open(&new_uri.to_string()).unwrap(); webbrowser_open(new_uri.to_string());
return; return Ok(Response::new(Vec::new()))
} }
let client = DROP_CLIENT_SYNC.clone(); let client = DROP_CLIENT_SYNC.clone();
let response = client let response = match client
.request(request.method().clone(), new_uri.to_string()) .request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {web_token}")) .header("Authorization", format!("Bearer {web_token}"))
.headers(request.headers().clone()) .headers(request.headers().clone())
.send() .send() {
.unwrap(); Ok(response) => response,
Err(e) => {
warn!("Could not send response. Got {e} when sending");
return Err(e.status().unwrap_or(StatusCode::BAD_REQUEST))
},
};
let response_status = response.status(); let response_status = response.status();
let response_body = response.bytes().unwrap(); let response_body = match response.bytes() {
Ok(bytes) => bytes,
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
};
let http_response = Response::builder() let http_response = Response::builder()
.status(response_status) .status(response_status)
.body(response_body.to_vec()) .body(response_body.to_vec())
.unwrap(); .expect("Failed to build server proto response");
responder.respond(http_response); Ok(http_response)
} }

View File

@ -11,10 +11,7 @@ use serde::Deserialize;
use url::Url; use url::Url;
use crate::{ use crate::{
AppState, AppStatus, database::db::{borrow_db_mut_checked, DATA_ROOT_DIR}, error::remote_access_error::RemoteAccessError, lock, AppState, AppStatus
database::db::{DATA_ROOT_DIR, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError,
state_lock,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -50,9 +47,10 @@ fn fetch_certificates() -> Vec<Certificate> {
} }
} }
.read_to_end(&mut buf) .read_to_end(&mut buf)
.expect(&format!( .unwrap_or_else(|e| panic!(
"Failed to read to end of certificate file {}", "Failed to read to end of certificate file {} with error {}",
c.path().display() c.path().display(),
e
)); ));
match Certificate::from_pem_bundle(&buf) { match Certificate::from_pem_bundle(&buf) {
@ -136,7 +134,7 @@ pub async fn use_remote_logic(
return Err(RemoteAccessError::InvalidEndpoint); return Err(RemoteAccessError::InvalidEndpoint);
} }
let mut app_state = state_lock!(state); let mut app_state = lock!(state);
app_state.status = AppStatus::SignedOut; app_state.status = AppStatus::SignedOut;
drop(app_state); drop(app_state);

View File

@ -0,0 +1,6 @@
#[macro_export]
macro_rules! app_emit {
($app:expr, $event:expr, $p:expr) => {
$app.emit($event, $p).expect(&format!("Failed to emit event {}", $event));
};
}

View File

@ -0,0 +1,6 @@
#[macro_export]
macro_rules! send {
($download_manager:expr, $signal:expr) => {
$download_manager.send($signal).unwrap_or_else(|_| panic!("Failed to send signal {} to the download manager", stringify!(signal)))
};
}

View File

@ -0,0 +1,6 @@
#[macro_export]
macro_rules! lock {
($mutex:expr) => {
$mutex.lock().unwrap_or_else(|_| panic!("Failed to lock onto {}", stringify!($mutex)))
};
}

View File

@ -1 +1,4 @@
pub mod state_lock; mod app_emit;
mod download_manager_send;
mod lock;
pub mod webbrowser_open;

View File

@ -1,6 +0,0 @@
#[macro_export]
macro_rules! state_lock {
($state:expr) => {
$state.lock().expect("Failed to lock onto state")
};
}

View File

@ -0,0 +1,7 @@
use log::warn;
pub fn webbrowser_open<T: AsRef<str>>(url: T) {
if let Err(e) = webbrowser::open(url.as_ref()) {
warn!("Could not open web browser to url {} with error {}", url.as_ref(), e);
};
}

5255
yarn.lock

File diff suppressed because it is too large Load Diff