mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 00:02:41 +10:00
Compare commits
7 Commits
main
...
a2d1a989e0
| Author | SHA1 | Date | |
|---|---|---|---|
| a2d1a989e0 | |||
| 83d2301056 | |||
| 87bbe1da49 | |||
| cc57ca7076 | |||
| 70cecdad19 | |||
| 3f18d15d39 | |||
| 97b5cd5e78 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -29,4 +29,6 @@ src-tauri/flamegraph.svg
|
|||||||
src-tauri/perf*
|
src-tauri/perf*
|
||||||
|
|
||||||
/*.AppImage
|
/*.AppImage
|
||||||
/squashfs-root
|
/squashfs-root
|
||||||
|
|
||||||
|
/target/
|
||||||
|
|||||||
24
README.md
24
README.md
@ -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).
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
:key="nav.id"
|
||||||
: nav.isInstalled.value
|
class="first:pt-0 last:pb-0"
|
||||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
v-slot="{ open }"
|
||||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
:default-open="nav.deft"
|
||||||
]" :href="nav.route">
|
>
|
||||||
<div class="flex items-center w-full gap-x-3">
|
<dt>
|
||||||
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
|
<DisclosureButton
|
||||||
<img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
|
||||||
:src="icons[nav.id]" alt="" />
|
>
|
||||||
</div>
|
<span class="text-sm font-semibold font-display">{{
|
||||||
<div class="flex flex-col flex-1">
|
nav.name
|
||||||
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
|
}}</span>
|
||||||
{{ nav.label }}
|
<span class="ml-6 flex h-7 items-center">
|
||||||
</p>
|
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||||
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
|
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
|
||||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
</span>
|
||||||
</p>
|
</DisclosureButton>
|
||||||
</div>
|
</dt>
|
||||||
</div>
|
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
|
||||||
</NuxtLink>
|
<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'
|
||||||
|
: item.isInstalled.value
|
||||||
|
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||||
|
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||||
|
]"
|
||||||
|
:href="item.route"
|
||||||
|
>
|
||||||
|
<div class="flex items-center w-full gap-x-2">
|
||||||
|
<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 class="inline-flex items-center gap-x-2">
|
||||||
|
<p
|
||||||
|
class="text-sm whitespace-nowrap font-display font-semibold"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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,36 +223,43 @@ 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 status = games[game.id].status;
|
const items = collection.entries.map(({ game }) => {
|
||||||
|
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 = {
|
||||||
label: game.mName,
|
label: game.mName,
|
||||||
route: `/library/${game.id}`,
|
route: `/library/${game.id}`,
|
||||||
prefix: `/library/${game.id}`,
|
prefix: `/library/${game.id}`,
|
||||||
isInstalled,
|
isInstalled,
|
||||||
id: game.id,
|
id: game.id,
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: collection.id,
|
||||||
|
name: collection.name,
|
||||||
|
deft: collection.isDefault,
|
||||||
|
items,
|
||||||
};
|
};
|
||||||
return item;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
<template></template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const loading = useLoadingIndicator();
|
|
||||||
|
|
||||||
watch(loading.isLoading, console.log);
|
|
||||||
</script>
|
|
||||||
@ -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', () => []);
|
||||||
@ -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;
|
||||||
|
|||||||
@ -116,7 +116,7 @@ platformInfo.value = currentPlatform;
|
|||||||
async function openDataDir() {
|
async function openDataDir() {
|
||||||
if (!dataDir.value) return;
|
if (!dataDir.value) return;
|
||||||
try {
|
try {
|
||||||
await open(dataDir.value);
|
await invoke("open_fs", { path: dataDir.value });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to open data dir:", error);
|
console.error("Failed to open data dir:", error);
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ async function openLogFile() {
|
|||||||
if (!dataDir.value) return;
|
if (!dataDir.value) return;
|
||||||
try {
|
try {
|
||||||
const logPath = `${dataDir.value}/drop.log`;
|
const logPath = `${dataDir.value}/drop.log`;
|
||||||
await open(logPath);
|
await invoke("open_fs", { path: logPath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to open log file:", error);
|
console.error("Failed to open log file:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,12 +9,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.7.0",
|
"@tauri-apps/api": "^2.7.0",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
"@tauri-apps/plugin-deep-link": "^2.4.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||||
"@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"
|
||||||
|
|||||||
2447
src-tauri/Cargo.lock
generated
2447
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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,8 +73,21 @@ 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" }
|
||||||
|
|
||||||
|
|
||||||
|
# Workspaces
|
||||||
|
client = { version = "0.1.0", path = "./client" }
|
||||||
|
database = { path = "./database" }
|
||||||
|
process = { path = "./process" }
|
||||||
|
remote = { version = "0.1.0", path = "./remote" }
|
||||||
|
utils = { path = "./utils" }
|
||||||
|
games = { version = "0.1.0", path = "./games" }
|
||||||
|
download_manager = { version = "0.1.0", path = "./download_manager" }
|
||||||
|
|
||||||
[dependencies.dynfmt]
|
[dependencies.dynfmt]
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
features = ["curly"]
|
features = ["curly"]
|
||||||
@ -106,7 +119,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"
|
||||||
@ -116,3 +137,18 @@ features = ["derive", "rc"]
|
|||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
|
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"client",
|
||||||
|
"database",
|
||||||
|
"process",
|
||||||
|
"remote",
|
||||||
|
"utils",
|
||||||
|
"cloud_saves",
|
||||||
|
"download_manager",
|
||||||
|
"games",
|
||||||
|
]
|
||||||
|
|
||||||
|
resolver = "3"
|
||||||
4862
src-tauri/client/Cargo.lock
generated
Normal file
4862
src-tauri/client/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
src-tauri/client/Cargo.toml
Normal file
12
src-tauri/client/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitcode = "0.6.7"
|
||||||
|
database = { version = "0.1.0", path = "../database" }
|
||||||
|
log = "0.4.28"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
tauri = "2.8.5"
|
||||||
|
tauri-plugin-autostart = "2.5.0"
|
||||||
12
src-tauri/client/src/app_status.rs
Normal file
12
src-tauri/client/src/app_status.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
|
||||||
|
pub enum AppStatus {
|
||||||
|
NotConfigured,
|
||||||
|
Offline,
|
||||||
|
ServerError,
|
||||||
|
SignedOut,
|
||||||
|
SignedIn,
|
||||||
|
SignedInNeedsReauth,
|
||||||
|
ServerUnavailable,
|
||||||
|
}
|
||||||
26
src-tauri/client/src/autostart.rs
Normal file
26
src-tauri/client/src/autostart.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use database::borrow_db_checked;
|
||||||
|
use log::debug;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
|
||||||
|
// New function to sync state on startup
|
||||||
|
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||||
|
let db_handle = borrow_db_checked();
|
||||||
|
let should_be_enabled = db_handle.settings.autostart;
|
||||||
|
drop(db_handle);
|
||||||
|
|
||||||
|
let manager = app.autolaunch();
|
||||||
|
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if current_state != should_be_enabled {
|
||||||
|
if should_be_enabled {
|
||||||
|
manager.enable().map_err(|e| e.to_string())?;
|
||||||
|
debug!("synced autostart: enabled");
|
||||||
|
} else {
|
||||||
|
manager.disable().map_err(|e| e.to_string())?;
|
||||||
|
debug!("synced autostart: disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
52
src-tauri/client/src/compat.rs
Normal file
52
src-tauri/client/src/compat.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::OsStr,
|
||||||
|
path::PathBuf,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
pub static COMPAT_INFO: LazyLock<Option<CompatInfo>> = LazyLock::new(create_new_compat_info);
|
||||||
|
|
||||||
|
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
|
||||||
|
let x = get_umu_executable();
|
||||||
|
info!("{:?}", &x);
|
||||||
|
x
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CompatInfo {
|
||||||
|
pub umu_installed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_new_compat_info() -> Option<CompatInfo> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return None;
|
||||||
|
|
||||||
|
let has_umu_installed = UMU_LAUNCHER_EXECUTABLE.is_some();
|
||||||
|
Some(CompatInfo {
|
||||||
|
umu_installed: has_umu_installed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const UMU_BASE_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
||||||
|
const UMU_INSTALL_DIRS: [&str; 4] = ["/app/share", "/use/local/share", "/usr/share", "/opt"];
|
||||||
|
|
||||||
|
fn get_umu_executable() -> Option<PathBuf> {
|
||||||
|
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
|
||||||
|
return Some(PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in UMU_INSTALL_DIRS {
|
||||||
|
let p = PathBuf::from(dir).join(UMU_BASE_LAUNCHER_EXECUTABLE);
|
||||||
|
if check_executable_exists(&p) {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
|
||||||
|
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).output();
|
||||||
|
has_umu_installed.is_ok()
|
||||||
|
}
|
||||||
4
src-tauri/client/src/lib.rs
Normal file
4
src-tauri/client/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod app_status;
|
||||||
|
pub mod autostart;
|
||||||
|
pub mod compat;
|
||||||
|
pub mod user;
|
||||||
12
src-tauri/client/src/user.rs
Normal file
12
src-tauri/client/src/user.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use bitcode::{Decode, Encode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct User {
|
||||||
|
id: String,
|
||||||
|
username: String,
|
||||||
|
admin: bool,
|
||||||
|
display_name: String,
|
||||||
|
profile_picture_object_id: String,
|
||||||
|
}
|
||||||
19
src-tauri/cloud_saves/Cargo.toml
Normal file
19
src-tauri/cloud_saves/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "cloud_saves"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
database = { version = "0.1.0", path = "../database" }
|
||||||
|
dirs = "6.0.0"
|
||||||
|
log = "0.4.28"
|
||||||
|
regex = "1.11.3"
|
||||||
|
rustix = "1.1.2"
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
serde_with = "3.15.0"
|
||||||
|
tar = "0.4.44"
|
||||||
|
tempfile = "3.23.0"
|
||||||
|
uuid = "1.18.1"
|
||||||
|
whoami = "1.6.1"
|
||||||
|
zstd = "0.13.3"
|
||||||
234
src-tauri/cloud_saves/src/backup_manager.rs
Normal file
234
src-tauri/cloud_saves/src/backup_manager.rs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use database::platform::Platform;
|
||||||
|
use database::{GameVersion, db::DATA_ROOT_DIR};
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::error::BackupError;
|
||||||
|
|
||||||
|
use super::path::CommonPath;
|
||||||
|
|
||||||
|
pub struct BackupManager<'a> {
|
||||||
|
pub current_platform: Platform,
|
||||||
|
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BackupManager<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupManager<'_> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
BackupManager {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
current_platform: Platform::Windows,
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
current_platform: Platform::macOS,
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
current_platform: Platform::Linux,
|
||||||
|
|
||||||
|
sources: HashMap::from([
|
||||||
|
// Current platform to target platform
|
||||||
|
(
|
||||||
|
(Platform::Windows, Platform::Windows),
|
||||||
|
&WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(Platform::Linux, Platform::Linux),
|
||||||
|
&LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(Platform::macOS, Platform::macOS),
|
||||||
|
&MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BackupHandler: Send + Sync {
|
||||||
|
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(DATA_ROOT_DIR.join("games"))
|
||||||
|
}
|
||||||
|
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(PathBuf::from_str(&game.game_id).unwrap())
|
||||||
|
}
|
||||||
|
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(self
|
||||||
|
.root_translate(path, game)?
|
||||||
|
.join(self.game_translate(path, game)?))
|
||||||
|
}
|
||||||
|
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
||||||
|
let c = CommonPath::Home.get().ok_or(BackupError::NotFound);
|
||||||
|
println!("{:?}", c);
|
||||||
|
c
|
||||||
|
}
|
||||||
|
fn store_user_id_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError)
|
||||||
|
}
|
||||||
|
fn os_user_name_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(PathBuf::from_str(&whoami::username()).unwrap())
|
||||||
|
}
|
||||||
|
fn win_app_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winAppData>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn win_local_app_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winLocalAppData>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn win_local_app_data_low_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winLocalAppDataLow>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn win_documents_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winDocuments>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn win_public_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winPublic>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn win_program_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winProgramData>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn win_dir_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected Windows Reference in Backup <winDir>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn xdg_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected XDG Reference in Backup <xdgData>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn xdg_config_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
warn!("Unexpected XDG Reference in Backup <xdgConfig>");
|
||||||
|
Err(BackupError::InvalidSystem)
|
||||||
|
}
|
||||||
|
fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(PathBuf::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LinuxBackupManager {}
|
||||||
|
impl BackupHandler for LinuxBackupManager {
|
||||||
|
fn xdg_config_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::Data.get().ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
fn xdg_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::Config.get().ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct WindowsBackupManager {}
|
||||||
|
impl BackupHandler for WindowsBackupManager {
|
||||||
|
fn win_app_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::Config.get().ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
fn win_local_app_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::DataLocal.get().ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
fn win_local_app_data_low_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::DataLocalLow
|
||||||
|
.get()
|
||||||
|
.ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
fn win_dir_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(PathBuf::from_str("C:/Windows").unwrap())
|
||||||
|
}
|
||||||
|
fn win_documents_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::Document.get().ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
fn win_program_data_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
Ok(PathBuf::from_str("C:/ProgramData").unwrap())
|
||||||
|
}
|
||||||
|
fn win_public_translate(
|
||||||
|
&self,
|
||||||
|
_path: &PathBuf,
|
||||||
|
_game: &GameVersion,
|
||||||
|
) -> Result<PathBuf, BackupError> {
|
||||||
|
CommonPath::Public.get().ok_or(BackupError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct MacBackupManager {}
|
||||||
|
impl BackupHandler for MacBackupManager {}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
use crate::process::process_manager::Platform;
|
use database::platform::Platform;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum Condition {
|
pub enum Condition {
|
||||||
Os(Platform)
|
Os(Platform),
|
||||||
|
Other
|
||||||
}
|
}
|
||||||
27
src-tauri/cloud_saves/src/error.rs
Normal file
27
src-tauri/cloud_saves/src/error.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
|
#[derive(Debug, SerializeDisplay, Clone, Copy)]
|
||||||
|
|
||||||
|
pub enum BackupError {
|
||||||
|
InvalidSystem,
|
||||||
|
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
ParseError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BackupError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
BackupError::InvalidSystem => "Attempted to generate path for invalid system",
|
||||||
|
|
||||||
|
BackupError::NotFound => "Could not generate or find path",
|
||||||
|
|
||||||
|
BackupError::ParseError => "Failed to parse path",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src-tauri/cloud_saves/src/lib.rs
Normal file
8
src-tauri/cloud_saves/src/lib.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
pub mod backup_manager;
|
||||||
|
pub mod conditions;
|
||||||
|
pub mod error;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod normalise;
|
||||||
|
pub mod path;
|
||||||
|
pub mod placeholder;
|
||||||
|
pub mod resolver;
|
||||||
@ -1,7 +1,6 @@
|
|||||||
use crate::database::db::GameVersion;
|
use database::GameVersion;
|
||||||
|
|
||||||
use super::conditions::{Condition};
|
|
||||||
|
|
||||||
|
use super::conditions::Condition;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct CloudSaveMetadata {
|
pub struct CloudSaveMetadata {
|
||||||
@ -16,15 +15,17 @@ pub struct GameFile {
|
|||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
pub data_type: DataType,
|
pub data_type: DataType,
|
||||||
pub tags: Vec<Tag>,
|
pub tags: Vec<Tag>,
|
||||||
pub conditions: Vec<Condition>
|
pub conditions: Vec<Condition>,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum DataType {
|
pub enum DataType {
|
||||||
Registry,
|
Registry,
|
||||||
File,
|
File,
|
||||||
Other
|
Other,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
#[derive(
|
||||||
|
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum Tag {
|
pub enum Tag {
|
||||||
Config,
|
Config,
|
||||||
@ -32,4 +33,4 @@ pub enum Tag {
|
|||||||
#[default]
|
#[default]
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
@ -1,11 +1,10 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use database::platform::Platform;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use crate::process::process_manager::Platform;
|
|
||||||
|
|
||||||
use super::placeholder::*;
|
use super::placeholder::*;
|
||||||
|
|
||||||
|
|
||||||
pub fn normalize(path: &str, os: Platform) -> String {
|
pub fn normalize(path: &str, os: Platform) -> String {
|
||||||
let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/");
|
let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/");
|
||||||
|
|
||||||
@ -14,18 +13,25 @@ pub fn normalize(path: &str, os: Platform) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static CONSECUTIVE_SLASHES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/{2,}").unwrap());
|
static CONSECUTIVE_SLASHES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/{2,}").unwrap());
|
||||||
static UNNECESSARY_DOUBLE_STAR_1: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap());
|
static UNNECESSARY_DOUBLE_STAR_1: LazyLock<Regex> =
|
||||||
static UNNECESSARY_DOUBLE_STAR_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap());
|
LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap());
|
||||||
|
static UNNECESSARY_DOUBLE_STAR_2: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap());
|
||||||
static ENDING_WILDCARD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap());
|
static ENDING_WILDCARD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap());
|
||||||
static ENDING_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap());
|
static ENDING_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap());
|
||||||
static INTERMEDIATE_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\./)").unwrap());
|
static INTERMEDIATE_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\./)").unwrap());
|
||||||
static BLANK_SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap());
|
static BLANK_SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap());
|
||||||
static APP_DATA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap());
|
static APP_DATA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap());
|
||||||
static APP_DATA_ROAMING: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap());
|
static APP_DATA_ROAMING: LazyLock<Regex> =
|
||||||
static APP_DATA_LOCAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap());
|
LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap());
|
||||||
static APP_DATA_LOCAL_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap());
|
static APP_DATA_LOCAL: LazyLock<Regex> =
|
||||||
static USER_PROFILE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap());
|
LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap());
|
||||||
static DOCUMENTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap());
|
static APP_DATA_LOCAL_2: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap());
|
||||||
|
static USER_PROFILE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap());
|
||||||
|
static DOCUMENTS: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap());
|
||||||
|
|
||||||
for (pattern, replacement) in [
|
for (pattern, replacement) in [
|
||||||
(&CONSECUTIVE_SLASHES, "/"),
|
(&CONSECUTIVE_SLASHES, "/"),
|
||||||
@ -66,7 +72,9 @@ pub fn normalize(path: &str, os: Platform) -> String {
|
|||||||
|
|
||||||
fn too_broad(path: &str) -> bool {
|
fn too_broad(path: &str) -> bool {
|
||||||
println!("Path: {}", path);
|
println!("Path: {}", path);
|
||||||
use {BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA};
|
use {
|
||||||
|
BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA,
|
||||||
|
};
|
||||||
|
|
||||||
let path_lower = path.to_lowercase();
|
let path_lower = path.to_lowercase();
|
||||||
|
|
||||||
@ -77,7 +85,9 @@ fn too_broad(path: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for item in AVOID_WILDCARDS {
|
for item in AVOID_WILDCARDS {
|
||||||
if path.starts_with(&format!("{}/*", item)) || path.starts_with(&format!("{}/{}", item, STORE_USER_ID)) {
|
if path.starts_with(&format!("{}/*", item))
|
||||||
|
|| path.starts_with(&format!("{}/{}", item, STORE_USER_ID))
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,7 +134,6 @@ fn too_broad(path: &str) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Drive letters:
|
// Drive letters:
|
||||||
let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap();
|
let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap();
|
||||||
@ -159,4 +168,4 @@ pub fn usable(path: &str) -> bool {
|
|||||||
&& !path.starts_with("../")
|
&& !path.starts_with("../")
|
||||||
&& !too_broad(path)
|
&& !too_broad(path)
|
||||||
&& !unprintable.is_match(path)
|
&& !unprintable.is_match(path)
|
||||||
}
|
}
|
||||||
@ -13,12 +13,12 @@ pub enum CommonPath {
|
|||||||
|
|
||||||
impl CommonPath {
|
impl CommonPath {
|
||||||
pub fn get(&self) -> Option<PathBuf> {
|
pub fn get(&self) -> Option<PathBuf> {
|
||||||
static CONFIG: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::config_dir());
|
static CONFIG: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::config_dir);
|
||||||
static DATA: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_dir());
|
static DATA: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::data_dir);
|
||||||
static DATA_LOCAL: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_local_dir());
|
static DATA_LOCAL: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::data_local_dir);
|
||||||
static DOCUMENT: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::document_dir());
|
static DOCUMENT: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::document_dir);
|
||||||
static HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::home_dir());
|
static HOME: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::home_dir);
|
||||||
static PUBLIC: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::public_dir());
|
static PUBLIC: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::public_dir);
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static DATA_LOCAL_LOW: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
|
static DATA_LOCAL_LOW: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
|
||||||
@ -48,4 +48,4 @@ pub const XDG_DATA: &str = "<xdgData>"; // %WINDIR% on Windows
|
|||||||
pub const XDG_CONFIG: &str = "<xdgConfig>"; // $XDG_DATA_HOME on Linux
|
pub const XDG_CONFIG: &str = "<xdgConfig>"; // $XDG_DATA_HOME on Linux
|
||||||
pub const SKIP: &str = "<skip>"; // $XDG_CONFIG_HOME on Linux
|
pub const SKIP: &str = "<skip>"; // $XDG_CONFIG_HOME on Linux
|
||||||
|
|
||||||
pub static OS_USERNAME: LazyLock<String> = LazyLock::new(|| whoami::username());
|
pub static OS_USERNAME: LazyLock<String> = LazyLock::new(whoami::username);
|
||||||
@ -1,22 +1,17 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::{self, create_dir_all, File},
|
fs::{self, File, create_dir_all},
|
||||||
io::{self, ErrorKind, Read, Write},
|
io::{self, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
thread::sleep,
|
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use crate::error::BackupError;
|
||||||
backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*,
|
|
||||||
};
|
use super::{backup_manager::BackupHandler, placeholder::*};
|
||||||
|
use database::GameVersion;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use rustix::path::Arg;
|
use rustix::path::Arg;
|
||||||
use tempfile::tempfile;
|
use tempfile::tempfile;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::db::GameVersion, error::backup_error::BackupError, process::process_manager::Platform,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize};
|
use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize};
|
||||||
|
|
||||||
pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
|
pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
|
||||||
@ -31,7 +26,7 @@ pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
|
|||||||
.iter()
|
.iter()
|
||||||
.find_map(|p| match p {
|
.find_map(|p| match p {
|
||||||
super::conditions::Condition::Os(os) => Some(os),
|
super::conditions::Condition::Os(os) => Some(os),
|
||||||
_ => None,
|
_ => None
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
@ -64,7 +59,7 @@ pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
|
|||||||
let binding = serde_json::to_string(meta).unwrap();
|
let binding = serde_json::to_string(meta).unwrap();
|
||||||
let serialized = binding.as_bytes();
|
let serialized = binding.as_bytes();
|
||||||
let mut file = tempfile().unwrap();
|
let mut file = tempfile().unwrap();
|
||||||
file.write(serialized).unwrap();
|
file.write_all(serialized).unwrap();
|
||||||
tarball.append_file("metadata", &mut file).unwrap();
|
tarball.append_file("metadata", &mut file).unwrap();
|
||||||
tarball.into_inner().unwrap().finish().unwrap()
|
tarball.into_inner().unwrap().finish().unwrap()
|
||||||
}
|
}
|
||||||
@ -97,7 +92,7 @@ pub fn extract(file: PathBuf) -> Result<(), BackupError> {
|
|||||||
.iter()
|
.iter()
|
||||||
.find_map(|p| match p {
|
.find_map(|p| match p {
|
||||||
super::conditions::Condition::Os(os) => Some(os),
|
super::conditions::Condition::Os(os) => Some(os),
|
||||||
_ => None,
|
_ => None
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
@ -116,7 +111,7 @@ pub fn extract(file: PathBuf) -> Result<(), BackupError> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?;
|
let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?;
|
||||||
create_dir_all(&new_path.parent().unwrap()).unwrap();
|
create_dir_all(new_path.parent().unwrap()).unwrap();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Current path {:?} copying to {:?}",
|
"Current path {:?} copying to {:?}",
|
||||||
@ -133,23 +128,22 @@ pub fn copy_item<P: AsRef<Path>>(src: P, dest: P) -> io::Result<()> {
|
|||||||
let src_path = src.as_ref();
|
let src_path = src.as_ref();
|
||||||
let dest_path = dest.as_ref();
|
let dest_path = dest.as_ref();
|
||||||
|
|
||||||
let metadata = fs::metadata(&src_path)?;
|
let metadata = fs::metadata(src_path)?;
|
||||||
|
|
||||||
if metadata.is_file() {
|
if metadata.is_file() {
|
||||||
// Ensure the parent directory of the destination exists for a file copy
|
// Ensure the parent directory of the destination exists for a file copy
|
||||||
if let Some(parent) = dest_path.parent() {
|
if let Some(parent) = dest_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
fs::copy(&src_path, &dest_path)?;
|
fs::copy(src_path, dest_path)?;
|
||||||
} else if metadata.is_dir() {
|
} else if metadata.is_dir() {
|
||||||
// For directories, we call the recursive helper function.
|
// For directories, we call the recursive helper function.
|
||||||
// The destination for the recursive copy is the `dest_path` itself.
|
// The destination for the recursive copy is the `dest_path` itself.
|
||||||
copy_dir_recursive(&src_path, &dest_path)?;
|
copy_dir_recursive(src_path, dest_path)?;
|
||||||
} else {
|
} else {
|
||||||
// Handle other file types like symlinks if necessary,
|
// Handle other file types like symlinks if necessary,
|
||||||
// for now, return an error or skip.
|
// for now, return an error or skip.
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::other(
|
||||||
io::ErrorKind::Other,
|
|
||||||
format!("Source {:?} is neither a file nor a directory", src_path),
|
format!("Source {:?} is neither a file nor a directory", src_path),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -158,7 +152,7 @@ pub fn copy_item<P: AsRef<Path>>(src: P, dest: P) -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
||||||
fs::create_dir_all(&dest)?;
|
fs::create_dir_all(dest)?;
|
||||||
|
|
||||||
for entry in fs::read_dir(src)? {
|
for entry in fs::read_dir(src)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
@ -220,43 +214,3 @@ pub fn parse_path(
|
|||||||
println!("Final line: {:?}", &s);
|
println!("Final line: {:?}", &s);
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test() {
|
|
||||||
let mut meta = CloudSaveMetadata {
|
|
||||||
files: vec![
|
|
||||||
GameFile {
|
|
||||||
path: String::from("<home>/favicon.png"),
|
|
||||||
id: None,
|
|
||||||
data_type: super::metadata::DataType::File,
|
|
||||||
tags: Vec::new(),
|
|
||||||
conditions: vec![Condition::Os(Platform::Linux)],
|
|
||||||
},
|
|
||||||
GameFile {
|
|
||||||
path: String::from("<home>/Documents/Pixel Art"),
|
|
||||||
id: None,
|
|
||||||
data_type: super::metadata::DataType::File,
|
|
||||||
tags: Vec::new(),
|
|
||||||
conditions: vec![Condition::Os(Platform::Linux)],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
game_version: GameVersion {
|
|
||||||
game_id: String::new(),
|
|
||||||
version_name: String::new(),
|
|
||||||
platform: Platform::Linux,
|
|
||||||
launch_command: String::new(),
|
|
||||||
launch_args: Vec::new(),
|
|
||||||
launch_command_template: String::new(),
|
|
||||||
setup_command: String::new(),
|
|
||||||
setup_args: Vec::new(),
|
|
||||||
setup_command_template: String::new(),
|
|
||||||
only_setup: true,
|
|
||||||
version_index: 0,
|
|
||||||
delta: false,
|
|
||||||
umu_id_override: None,
|
|
||||||
},
|
|
||||||
save_id: String::from("aaaaaaa"),
|
|
||||||
};
|
|
||||||
//resolve(&mut meta);
|
|
||||||
|
|
||||||
extract("save".into()).unwrap();
|
|
||||||
}
|
|
||||||
0
src-tauri/cloud_saves/src/strict_path.rs
Normal file
0
src-tauri/cloud_saves/src/strict_path.rs
Normal file
15
src-tauri/database/Cargo.toml
Normal file
15
src-tauri/database/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "database"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.42"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
log = "0.4.28"
|
||||||
|
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
||||||
|
rustbreak = "2.0.0"
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_with = "3.15.0"
|
||||||
|
url = "2.5.7"
|
||||||
|
whoami = "1.6.1"
|
||||||
45
src-tauri/database/src/db.rs
Normal file
45
src-tauri/database/src/db.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rustbreak::{DeSerError, DeSerializer};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
use crate::interface::{DatabaseImpls, DatabaseInterface};
|
||||||
|
|
||||||
|
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
static DATA_ROOT_PREFIX: &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
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct DropDatabaseSerializer;
|
||||||
|
|
||||||
|
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||||
|
for DropDatabaseSerializer
|
||||||
|
{
|
||||||
|
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||||
|
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
s.read_to_end(&mut buf)
|
||||||
|
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||||
|
let (val, _version) =
|
||||||
|
native_model::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,44 +3,18 @@ use std::{
|
|||||||
mem::ManuallyDrop,
|
mem::ManuallyDrop,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
|
sync::{RwLockReadGuard, RwLockWriteGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
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::{PathDatabase, RustbreakError};
|
||||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::DB;
|
use crate::{
|
||||||
|
db::{DATA_ROOT_DIR, DB, DropDatabaseSerializer},
|
||||||
use super::models::data::Database;
|
models::data::Database,
|
||||||
|
};
|
||||||
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
|
|
||||||
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop")));
|
|
||||||
|
|
||||||
// Custom JSON serializer to support everything we need
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct DropDatabaseSerializer;
|
|
||||||
|
|
||||||
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
|
||||||
for DropDatabaseSerializer
|
|
||||||
{
|
|
||||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
|
||||||
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
|
||||||
.map_err(|e| DeSerError::Internal(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
s.read_to_end(&mut buf)
|
|
||||||
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
|
||||||
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
|
||||||
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
|
||||||
Ok(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type DatabaseInterface =
|
pub type DatabaseInterface =
|
||||||
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
|
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
|
||||||
@ -59,13 +33,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 +84,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 +115,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")
|
||||||
}
|
}
|
||||||
14
src-tauri/database/src/lib.rs
Normal file
14
src-tauri/database/src/lib.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#![feature(nonpoison_rwlock)]
|
||||||
|
|
||||||
|
pub mod db;
|
||||||
|
pub mod debug;
|
||||||
|
pub mod interface;
|
||||||
|
pub mod models;
|
||||||
|
pub mod platform;
|
||||||
|
|
||||||
|
pub use db::DB;
|
||||||
|
pub use interface::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
|
pub use models::data::{
|
||||||
|
ApplicationTransientStatus, Database, DatabaseApplications, DatabaseAuth, DownloadType,
|
||||||
|
DownloadableMetadata, GameDownloadStatus, GameVersion, Settings,
|
||||||
|
};
|
||||||
@ -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,25 +14,41 @@ 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 {
|
||||||
use crate::process::process_manager::Platform;
|
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 serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::platform::Platform;
|
||||||
|
|
||||||
use super::{Deserialize, Serialize, native_model};
|
use super::{Deserialize, Serialize, native_model};
|
||||||
|
|
||||||
fn default_template() -> String {
|
fn default_template() -> String {
|
||||||
"{}".to_owned()
|
"{}".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[native_model(id = 2, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
#[native_model(id = 2, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
pub struct GameVersion {
|
pub struct GameVersion {
|
||||||
@ -116,6 +127,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 +156,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 +186,19 @@ 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::{Deserialize, Serialize, native_model, v1};
|
||||||
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
|
|
||||||
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 +207,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 +230,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 +270,18 @@ 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 {
|
||||||
@ -292,22 +302,19 @@ pub mod data {
|
|||||||
mod v3 {
|
mod v3 {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::{
|
use super::{Deserialize, Serialize, native_model, v1, v2};
|
||||||
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
|
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde, from = v2::Database)]
|
||||||
Settings, native_model, v2,
|
|
||||||
};
|
|
||||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
#[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 {
|
||||||
@ -348,4 +355,19 @@ pub mod data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl DatabaseAuth {
|
||||||
|
pub fn new(
|
||||||
|
private: String,
|
||||||
|
cert: String,
|
||||||
|
client_id: String,
|
||||||
|
web_token: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
private,
|
||||||
|
cert,
|
||||||
|
client_id,
|
||||||
|
web_token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
46
src-tauri/database/src/platform.rs
Normal file
46
src-tauri/database/src/platform.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Platform {
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
macOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub const HOST: Platform = Self::Windows;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub const HOST: Platform = Self::macOS;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub const HOST: Platform = Self::Linux;
|
||||||
|
|
||||||
|
pub fn is_case_sensitive(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Windows | Self::macOS => false,
|
||||||
|
Self::Linux => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Platform {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
match value.to_lowercase().trim() {
|
||||||
|
"windows" => Self::Windows,
|
||||||
|
"linux" => Self::Linux,
|
||||||
|
"mac" | "macos" => Self::macOS,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<whoami::Platform> for Platform {
|
||||||
|
fn from(value: whoami::Platform) -> Self {
|
||||||
|
match value {
|
||||||
|
whoami::Platform::Windows => Platform::Windows,
|
||||||
|
whoami::Platform::Linux => Platform::Linux,
|
||||||
|
whoami::Platform::MacOS => Platform::macOS,
|
||||||
|
platform => unimplemented!("Playform {} is not supported", platform),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src-tauri/download_manager/Cargo.toml
Normal file
17
src-tauri/download_manager/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "download_manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
atomic-instant-full = "0.1.0"
|
||||||
|
database = { version = "0.1.0", path = "../database" }
|
||||||
|
humansize = "2.1.3"
|
||||||
|
log = "0.4.28"
|
||||||
|
parking_lot = "0.12.5"
|
||||||
|
remote = { version = "0.1.0", path = "../remote" }
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_with = "3.15.0"
|
||||||
|
tauri = "2.8.5"
|
||||||
|
throttle_my_fn = "0.2.6"
|
||||||
|
utils = { version = "0.1.0", path = "../utils" }
|
||||||
@ -7,13 +7,15 @@ use std::{
|
|||||||
thread::{JoinHandle, spawn},
|
thread::{JoinHandle, spawn},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use database::DownloadableMetadata;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::AppHandle;
|
||||||
|
use utils::{app_emit, lock, send};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
download_manager_frontend::DownloadStatus,
|
||||||
error::application_download_error::ApplicationDownloadError,
|
error::ApplicationDownloadError,
|
||||||
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
frontend_updates::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -75,7 +77,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 +96,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 +106,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 +120,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 +138,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 +196,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 +213,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 +227,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 +257,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 +281,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 +292,11 @@ impl DownloadManagerBuilder {
|
|||||||
|
|
||||||
if validate_result {
|
if validate_result {
|
||||||
download_agent.on_complete(&app_handle);
|
download_agent.on_complete(&app_handle);
|
||||||
sender
|
send!(
|
||||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
sender,
|
||||||
.unwrap();
|
DownloadManagerSignal::Completed(download_agent.metadata())
|
||||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
);
|
||||||
|
send!(sender, DownloadManagerSignal::UpdateUIQueue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,22 +316,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(¤t_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,36 +341,27 @@ 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 ¤t_download.metadata() == meta {
|
if let Some(current_metadata) = self.download_queue.read().front()
|
||||||
self.set_status(DownloadManagerStatus::Paused);
|
&& current_metadata == meta
|
||||||
current_download.on_cancelled(&self.app_handle);
|
&& let Some(current_download) = self.download_agent_registry.get(current_metadata)
|
||||||
self.stop_and_wait_current_download();
|
{
|
||||||
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
|
current_download.on_cancelled(&self.app_handle);
|
||||||
|
self.stop_and_wait_current_download();
|
||||||
|
|
||||||
self.download_queue.pop_front();
|
self.download_queue.pop_front();
|
||||||
|
|
||||||
self.cleanup_current_download();
|
self.cleanup_current_download();
|
||||||
debug!("current download queue: {:?}", self.download_queue.read());
|
self.download_agent_registry.remove(meta);
|
||||||
}
|
debug!("current download queue: {:?}", self.download_queue.read());
|
||||||
// TODO: Collapse these two into a single if statement somehow
|
}
|
||||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
// else just cancel it
|
||||||
let index = self.download_queue.get_by_meta(meta);
|
else if let Some(download_agent) = self.download_agent_registry.get(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);
|
|
||||||
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);
|
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);
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
debug!(
|
debug!(
|
||||||
"removed {:?} from queue {:?}",
|
"removed {:?} from queue {:?}",
|
||||||
@ -371,12 +370,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 +395,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,19 +3,18 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use database::DownloadableMetadata;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utils::{lock, send};
|
||||||
|
|
||||||
use crate::{
|
use crate::error::ApplicationDownloadError;
|
||||||
database::models::data::DownloadableMetadata,
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
||||||
@ -62,7 +61,7 @@ impl Serialize for DownloadManagerStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||||
pub enum DownloadStatus {
|
pub enum DownloadStatus {
|
||||||
Queued,
|
Queued,
|
||||||
Downloading,
|
Downloading,
|
||||||
@ -80,6 +79,7 @@ pub enum DownloadStatus {
|
|||||||
/// The actual download queue may be accessed through the .`edit()` function,
|
/// The actual download queue may be accessed through the .`edit()` function,
|
||||||
/// which provides raw access to the underlying queue.
|
/// which provides raw access to the underlying queue.
|
||||||
/// THIS EDITING IS BLOCKING!!!
|
/// THIS EDITING IS BLOCKING!!!
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct DownloadManager {
|
pub struct DownloadManager {
|
||||||
terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
|
terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
|
||||||
download_queue: Queue,
|
download_queue: Queue,
|
||||||
@ -119,22 +119,21 @@ 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 =
|
||||||
let to_move = queue.remove(current_index).unwrap();
|
get_index_from_id(&mut queue, meta).expect("Failed to get meta index from id");
|
||||||
|
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 +142,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> {
|
||||||
@ -1,17 +1,21 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use database::DownloadableMetadata;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::{
|
use crate::error::ApplicationDownloadError;
|
||||||
database::models::data::DownloadableMetadata,
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_frontend::DownloadStatus,
|
download_manager_frontend::DownloadStatus,
|
||||||
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 +24,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);
|
||||||
80
src-tauri/download_manager/src/error.rs
Normal file
80
src-tauri/download_manager/src/error.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use humansize::{BINARY, format_size};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
io,
|
||||||
|
sync::{Arc, mpsc::SendError},
|
||||||
|
};
|
||||||
|
|
||||||
|
use remote::error::RemoteAccessError;
|
||||||
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
|
#[derive(SerializeDisplay)]
|
||||||
|
pub enum DownloadManagerError<T> {
|
||||||
|
IOError(io::Error),
|
||||||
|
SignalError(SendError<T>),
|
||||||
|
}
|
||||||
|
impl<T> Display for DownloadManagerError<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DownloadManagerError::IOError(error) => write!(f, "{error}"),
|
||||||
|
DownloadManagerError::SignalError(send_error) => write!(f, "{send_error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<SendError<T>> for DownloadManagerError<T> {
|
||||||
|
fn from(value: SendError<T>) -> Self {
|
||||||
|
DownloadManagerError::SignalError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<io::Error> for DownloadManagerError<T> {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
DownloadManagerError::IOError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Rename / separate from downloads
|
||||||
|
#[derive(Debug, SerializeDisplay)]
|
||||||
|
pub enum ApplicationDownloadError {
|
||||||
|
NotInitialized,
|
||||||
|
Communication(RemoteAccessError),
|
||||||
|
DiskFull(u64, u64),
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Checksum,
|
||||||
|
Lock,
|
||||||
|
IoError(Arc<io::Error>),
|
||||||
|
DownloadError(RemoteAccessError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ApplicationDownloadError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ApplicationDownloadError::NotInitialized => {
|
||||||
|
write!(f, "Download not initalized, did something go wrong?")
|
||||||
|
}
|
||||||
|
ApplicationDownloadError::DiskFull(required, available) => write!(
|
||||||
|
f,
|
||||||
|
"Game requires {}, {} remaining left on disk.",
|
||||||
|
format_size(*required, BINARY),
|
||||||
|
format_size(*available, BINARY),
|
||||||
|
),
|
||||||
|
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
|
||||||
|
ApplicationDownloadError::Lock => write!(
|
||||||
|
f,
|
||||||
|
"failed to acquire lock. Something has gone very wrong internally. Please restart the application"
|
||||||
|
),
|
||||||
|
ApplicationDownloadError::Checksum => {
|
||||||
|
write!(f, "checksum failed to validate for download")
|
||||||
|
}
|
||||||
|
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
||||||
|
ApplicationDownloadError::DownloadError(error) => {
|
||||||
|
write!(f, "Download failed with error {error:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for ApplicationDownloadError {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
ApplicationDownloadError::IoError(Arc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src-tauri/download_manager/src/frontend_updates.rs
Normal file
24
src-tauri/download_manager/src/frontend_updates.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use database::DownloadableMetadata;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::download_manager_frontend::DownloadStatus;
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct QueueUpdateEventQueueData {
|
||||||
|
pub meta: DownloadableMetadata,
|
||||||
|
pub status: DownloadStatus,
|
||||||
|
pub progress: f64,
|
||||||
|
pub current: usize,
|
||||||
|
pub max: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct QueueUpdateEvent {
|
||||||
|
pub queue: Vec<QueueUpdateEventQueueData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct StatsUpdateEvent {
|
||||||
|
pub speed: usize,
|
||||||
|
pub time: usize,
|
||||||
|
}
|
||||||
44
src-tauri/download_manager/src/lib.rs
Normal file
44
src-tauri/download_manager/src/lib.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#![feature(duration_millis_float)]
|
||||||
|
#![feature(nonpoison_mutex)]
|
||||||
|
#![feature(sync_nonpoison)]
|
||||||
|
|
||||||
|
use std::{ops::Deref, sync::OnceLock};
|
||||||
|
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
download_manager_builder::DownloadManagerBuilder, download_manager_frontend::DownloadManager,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod download_manager_builder;
|
||||||
|
pub mod download_manager_frontend;
|
||||||
|
pub mod downloadable;
|
||||||
|
pub mod error;
|
||||||
|
pub mod frontend_updates;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
|
pub static DOWNLOAD_MANAGER: DownloadManagerWrapper = DownloadManagerWrapper::new();
|
||||||
|
|
||||||
|
pub struct DownloadManagerWrapper(OnceLock<DownloadManager>);
|
||||||
|
impl DownloadManagerWrapper {
|
||||||
|
const fn new() -> Self {
|
||||||
|
DownloadManagerWrapper(OnceLock::new())
|
||||||
|
}
|
||||||
|
pub fn init(app_handle: AppHandle) {
|
||||||
|
DOWNLOAD_MANAGER
|
||||||
|
.0
|
||||||
|
.set(DownloadManagerBuilder::build(app_handle))
|
||||||
|
.expect("Failed to initialise download manager");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for DownloadManagerWrapper {
|
||||||
|
type Target = DownloadManager;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self.0.get() {
|
||||||
|
Some(download_manager) => download_manager,
|
||||||
|
None => unreachable!("Download manager should always be initialised"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@ -22,7 +22,11 @@ impl From<DownloadThreadControlFlag> for bool {
|
|||||||
/// false => Stop
|
/// false => Stop
|
||||||
impl From<bool> for DownloadThreadControlFlag {
|
impl From<bool> for DownloadThreadControlFlag {
|
||||||
fn from(value: bool) -> Self {
|
fn from(value: bool) -> Self {
|
||||||
if value { DownloadThreadControlFlag::Go } else { DownloadThreadControlFlag::Stop }
|
if value {
|
||||||
|
DownloadThreadControlFlag::Go
|
||||||
|
} else {
|
||||||
|
DownloadThreadControlFlag::Stop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,12 +9,13 @@ use std::{
|
|||||||
|
|
||||||
use atomic_instant_full::AtomicInstant;
|
use atomic_instant_full::AtomicInstant;
|
||||||
use throttle_my_fn::throttle;
|
use throttle_my_fn::throttle;
|
||||||
|
use utils::{lock, send};
|
||||||
|
|
||||||
use crate::download_manager::download_manager_frontend::DownloadManagerSignal;
|
use crate::download_manager_frontend::DownloadManagerSignal;
|
||||||
|
|
||||||
use super::rolling_progress_updates::RollingProgressWindow;
|
use super::rolling_progress_updates::RollingProgressWindow;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ProgressObject {
|
pub struct ProgressObject {
|
||||||
max: Arc<Mutex<usize>>,
|
max: Arc<Mutex<usize>>,
|
||||||
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
|
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
|
||||||
@ -23,7 +24,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 +75,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 +87,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 +117,9 @@ 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 +127,18 @@ 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 +148,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();
|
|
||||||
}
|
}
|
||||||
@ -3,9 +3,10 @@ use std::{
|
|||||||
sync::{Arc, Mutex, MutexGuard},
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::database::models::data::DownloadableMetadata;
|
use database::DownloadableMetadata;
|
||||||
|
use utils::lock;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
inner: Arc<Mutex<VecDeque<DownloadableMetadata>>>,
|
inner: Arc<Mutex<VecDeque<DownloadableMetadata>>>,
|
||||||
}
|
}
|
||||||
@ -24,10 +25,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()
|
||||||
@ -1,13 +1,19 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RollingProgressWindow<const S: usize> {
|
pub struct RollingProgressWindow<const S: usize> {
|
||||||
window: Arc<[AtomicUsize; S]>,
|
window: Arc<[AtomicUsize; S]>,
|
||||||
current: Arc<AtomicUsize>,
|
current: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
impl<const S: usize> Default for RollingProgressWindow<S> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<const S: usize> RollingProgressWindow<S> {
|
impl<const S: usize> RollingProgressWindow<S> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -22,17 +28,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 < ¤t)
|
.filter(|(i, _)| i < ¤t)
|
||||||
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
26
src-tauri/games/Cargo.toml
Normal file
26
src-tauri/games/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "games"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
atomic-instant-full = "0.1.0"
|
||||||
|
bitcode = "0.6.7"
|
||||||
|
boxcar = "0.2.14"
|
||||||
|
database = { version = "0.1.0", path = "../database" }
|
||||||
|
download_manager = { version = "0.1.0", path = "../download_manager" }
|
||||||
|
hex = "0.4.3"
|
||||||
|
log = "0.4.28"
|
||||||
|
md5 = "0.8.0"
|
||||||
|
rayon = "1.11.0"
|
||||||
|
remote = { version = "0.1.0", path = "../remote" }
|
||||||
|
reqwest = "0.12.23"
|
||||||
|
rustix = "1.1.2"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_with = "3.15.0"
|
||||||
|
sysinfo = "0.37.2"
|
||||||
|
tauri = "2.8.5"
|
||||||
|
throttle_my_fn = "0.2.6"
|
||||||
|
utils = { version = "0.1.0", path = "../utils" }
|
||||||
|
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
|
||||||
|
serde_json = "1.0.145"
|
||||||
@ -1,10 +1,11 @@
|
|||||||
|
use bitcode::{Decode, Encode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::games::library::Game;
|
use crate::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,
|
||||||
@ -1,2 +1 @@
|
|||||||
pub mod collection;
|
pub mod collection;
|
||||||
pub mod commands;
|
|
||||||
@ -1,44 +1,48 @@
|
|||||||
use crate::auth::generate_authorization_header;
|
use database::{
|
||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
ApplicationTransientStatus, DownloadType, DownloadableMetadata, borrow_db_checked,
|
||||||
use crate::database::models::data::{
|
borrow_db_mut_checked,
|
||||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata,
|
|
||||||
};
|
};
|
||||||
use crate::download_manager::download_manager_frontend::{DownloadManagerSignal, DownloadStatus};
|
use download_manager::download_manager_frontend::{DownloadManagerSignal, DownloadStatus};
|
||||||
use crate::download_manager::downloadable::Downloadable;
|
use download_manager::downloadable::Downloadable;
|
||||||
use crate::download_manager::util::download_thread_control_flag::{
|
use download_manager::error::ApplicationDownloadError;
|
||||||
|
use download_manager::util::download_thread_control_flag::{
|
||||||
DownloadThreadControl, DownloadThreadControlFlag,
|
DownloadThreadControl, DownloadThreadControlFlag,
|
||||||
};
|
};
|
||||||
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
use download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
|
||||||
use crate::games::downloads::manifest::{
|
|
||||||
DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody,
|
|
||||||
};
|
|
||||||
use crate::games::downloads::validate::validate_game_chunk;
|
|
||||||
use crate::games::library::{on_game_complete, push_game_update, set_partially_installed};
|
|
||||||
use crate::games::state::GameStatusManager;
|
|
||||||
use crate::process::utils::get_disk_available;
|
|
||||||
use crate::remote::requests::generate_url;
|
|
||||||
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rayon::ThreadPoolBuilder;
|
use rayon::ThreadPoolBuilder;
|
||||||
|
use remote::auth::generate_authorization_header;
|
||||||
|
use remote::error::RemoteAccessError;
|
||||||
|
use remote::requests::generate_url;
|
||||||
|
use remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
||||||
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};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::AppHandle;
|
||||||
|
use utils::{app_emit, lock, send};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use rustix::fs::{FallocateFlags, fallocate};
|
use rustix::fs::{FallocateFlags, fallocate};
|
||||||
|
|
||||||
|
use crate::downloads::manifest::{
|
||||||
|
DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody,
|
||||||
|
};
|
||||||
|
use crate::downloads::utils::get_disk_available;
|
||||||
|
use crate::downloads::validate::validate_game_chunk;
|
||||||
|
use crate::library::{on_game_complete, push_game_update, set_partially_installed};
|
||||||
|
use crate::state::GameStatusManager;
|
||||||
|
|
||||||
use super::download_logic::download_game_bucket;
|
use super::download_logic::download_game_bucket;
|
||||||
use super::drop_data::DropData;
|
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 +87,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 +104,18 @@ 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,13 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
let download_context = download_contexts
|
let download_context =
|
||||||
.get(&bucket.version)
|
download_contexts.get(&bucket.version).unwrap_or_else(|| {
|
||||||
.ok_or(RemoteAccessError::CorruptedState)
|
panic!(
|
||||||
.unwrap();
|
"Could not get bucket version {}. Corrupted state.",
|
||||||
|
bucket.version
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
scope.spawn(move |_| {
|
scope.spawn(move |_| {
|
||||||
// 3 attempts
|
// 3 attempts
|
||||||
@ -460,11 +486,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 +504,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 +512,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 +561,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 +573,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 +593,7 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
send!(sender, DownloadManagerSignal::Error(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -591,7 +620,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 +630,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 +655,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 +689,28 @@ 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}");
|
||||||
|
send!(
|
||||||
|
self.sender,
|
||||||
|
DownloadManagerSignal::Error(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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,30 +1,32 @@
|
|||||||
use crate::download_manager::util::download_thread_control_flag::{
|
|
||||||
DownloadThreadControl, DownloadThreadControlFlag,
|
|
||||||
};
|
|
||||||
use crate::download_manager::util::progress_object::ProgressHandle;
|
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
|
||||||
use crate::error::drop_server_error::DropServerError;
|
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
|
||||||
use crate::games::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop};
|
|
||||||
use crate::remote::auth::generate_authorization_header;
|
|
||||||
use crate::remote::requests::generate_url;
|
|
||||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
|
||||||
use log::{info, warn};
|
|
||||||
use md5::{Context, Digest};
|
|
||||||
use reqwest::blocking::Response;
|
|
||||||
|
|
||||||
use std::fs::{Permissions, set_permissions};
|
use std::fs::{Permissions, set_permissions};
|
||||||
use std::io::Read;
|
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},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use download_manager::error::ApplicationDownloadError;
|
||||||
|
use download_manager::util::download_thread_control_flag::{
|
||||||
|
DownloadThreadControl, DownloadThreadControlFlag,
|
||||||
|
};
|
||||||
|
use download_manager::util::progress_object::ProgressHandle;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use md5::{Context, Digest};
|
||||||
|
use remote::auth::generate_authorization_header;
|
||||||
|
use remote::error::{DropServerError, RemoteAccessError};
|
||||||
|
use remote::requests::generate_url;
|
||||||
|
use remote::utils::DROP_CLIENT_SYNC;
|
||||||
|
use reqwest::blocking::Response;
|
||||||
|
|
||||||
|
use crate::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop};
|
||||||
|
|
||||||
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,
|
||||||
@ -47,7 +49,7 @@ impl DropWriter<File> {
|
|||||||
|
|
||||||
fn finish(mut self) -> io::Result<Digest> {
|
fn finish(mut self) -> io::Result<Digest> {
|
||||||
self.flush()?;
|
self.flush()?;
|
||||||
Ok(self.hasher.compute())
|
Ok(self.hasher.finalize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Write automatically pushes to file and hasher
|
// Write automatically pushes to file and hasher
|
||||||
@ -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,32 @@ 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(©_buffer[0..size])?;
|
destination.write_all(©_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 +149,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 +178,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 +217,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}");
|
||||||
"invalid number of Content-Lengths recieved: {i}, {lengths}"
|
return Err(ApplicationDownloadError::DownloadError(
|
||||||
);
|
RemoteAccessError::InvalidResponse(DropServerError {
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
status_code: 400,
|
||||||
|
status_message: format!(
|
||||||
|
"invalid number of Content-Lengths recieved: {i}, {lengths}"
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
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)))?;
|
||||||
@ -1,9 +1,13 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use native_model::{Decode, Encode};
|
use native_model::{Decode, Encode};
|
||||||
|
use utils::lock;
|
||||||
|
|
||||||
pub type DropData = v1::DropData;
|
pub type DropData = v1::DropData;
|
||||||
|
|
||||||
@ -49,7 +53,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 +80,15 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
29
src-tauri/games/src/downloads/error.rs
Normal file
29
src-tauri/games/src/downloads/error.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
|
#[derive(SerializeDisplay)]
|
||||||
|
pub enum LibraryError {
|
||||||
|
MetaNotFound(String),
|
||||||
|
VersionNotFound(String),
|
||||||
|
}
|
||||||
|
impl Display for LibraryError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
LibraryError::MetaNotFound(id) => {
|
||||||
|
format!(
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
pub mod commands;
|
|
||||||
pub mod download_agent;
|
pub mod download_agent;
|
||||||
mod download_logic;
|
mod download_logic;
|
||||||
pub mod drop_data;
|
pub mod drop_data;
|
||||||
|
pub mod error;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
|
pub mod utils;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
@ -1,10 +1,8 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{io, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use futures_lite::io;
|
use download_manager::error::ApplicationDownloadError;
|
||||||
use sysinfo::{Disk, DiskRefreshKind, Disks};
|
use sysinfo::{Disk, DiskRefreshKind, Disks};
|
||||||
|
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
|
||||||
|
|
||||||
pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownloadError> {
|
pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownloadError> {
|
||||||
let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::nothing().with_storage());
|
let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::nothing().with_storage());
|
||||||
|
|
||||||
@ -21,7 +19,7 @@ pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownlo
|
|||||||
return Ok(disk.available_space());
|
return Ok(disk.available_space());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(ApplicationDownloadError::IoError(Arc::new(io::Error::other(
|
Err(ApplicationDownloadError::IoError(Arc::new(
|
||||||
"could not find disk of path",
|
io::Error::other("could not find disk of path"),
|
||||||
))))
|
)))
|
||||||
}
|
}
|
||||||
@ -3,17 +3,17 @@ use std::{
|
|||||||
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::debug;
|
use download_manager::{
|
||||||
use md5::Context;
|
error::ApplicationDownloadError,
|
||||||
|
util::{
|
||||||
use crate::{
|
|
||||||
download_manager::util::{
|
|
||||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||||
progress_object::ProgressHandle,
|
progress_object::ProgressHandle,
|
||||||
},
|
},
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
games::downloads::manifest::DropValidateContext,
|
|
||||||
};
|
};
|
||||||
|
use log::debug;
|
||||||
|
use md5::Context;
|
||||||
|
|
||||||
|
use crate::downloads::manifest::DropValidateContext;
|
||||||
|
|
||||||
pub fn validate_game_chunk(
|
pub fn validate_game_chunk(
|
||||||
ctx: &DropValidateContext,
|
ctx: &DropValidateContext,
|
||||||
@ -22,7 +22,10 @@ pub fn validate_game_chunk(
|
|||||||
) -> Result<bool, ApplicationDownloadError> {
|
) -> Result<bool, ApplicationDownloadError> {
|
||||||
debug!(
|
debug!(
|
||||||
"Starting chunk validation {}, {}, {} #{}",
|
"Starting chunk validation {}, {}, {} #{}",
|
||||||
ctx.path.display(), ctx.index, ctx.offset, ctx.checksum
|
ctx.path.display(),
|
||||||
|
ctx.index,
|
||||||
|
ctx.offset,
|
||||||
|
ctx.checksum
|
||||||
);
|
);
|
||||||
// If we're paused
|
// If we're paused
|
||||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||||
@ -36,19 +39,18 @@ 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)?;
|
||||||
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap();
|
|
||||||
if !completed {
|
if !completed {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = hex::encode(hasher.compute().0);
|
let res = hex::encode(hasher.finalize().0);
|
||||||
if res != ctx.checksum {
|
if res != ctx.checksum {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
#![feature(iterator_try_collect)]
|
||||||
|
|
||||||
pub mod collections;
|
pub mod collections;
|
||||||
pub mod commands;
|
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
|
pub mod scan;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
300
src-tauri/games/src/library.rs
Normal file
300
src-tauri/games/src/library.rs
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
use bitcode::{Decode, Encode};
|
||||||
|
use database::{
|
||||||
|
ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||||
|
borrow_db_checked, borrow_db_mut_checked,
|
||||||
|
};
|
||||||
|
use log::{debug, error, warn};
|
||||||
|
use remote::{
|
||||||
|
auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url,
|
||||||
|
utils::DROP_CLIENT_SYNC,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::remove_dir_all;
|
||||||
|
use std::thread::spawn;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use utils::app_emit;
|
||||||
|
|
||||||
|
use crate::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct FetchGameStruct {
|
||||||
|
game: Game,
|
||||||
|
status: GameStatusWithTransient,
|
||||||
|
version: Option<GameVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchGameStruct {
|
||||||
|
pub fn new(game: Game, status: GameStatusWithTransient, version: Option<GameVersion>) -> Self {
|
||||||
|
Self {
|
||||||
|
game,
|
||||||
|
status,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Game {
|
||||||
|
id: String,
|
||||||
|
m_name: String,
|
||||||
|
m_short_description: String,
|
||||||
|
m_description: String,
|
||||||
|
// mDevelopers
|
||||||
|
// mPublishers
|
||||||
|
m_icon_object_id: String,
|
||||||
|
m_banner_object_id: String,
|
||||||
|
m_cover_object_id: String,
|
||||||
|
m_image_library_object_ids: Vec<String>,
|
||||||
|
m_image_carousel_object_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
impl Game {
|
||||||
|
pub fn id(&self) -> &String {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct GameUpdateEvent {
|
||||||
|
pub game_id: String,
|
||||||
|
pub status: (
|
||||||
|
Option<GameDownloadStatus>,
|
||||||
|
Option<ApplicationTransientStatus>,
|
||||||
|
),
|
||||||
|
pub version: Option<GameVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by:
|
||||||
|
* - on_cancel, when cancelled, for obvious reasons
|
||||||
|
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
|
||||||
|
* - when scanning, to import the game
|
||||||
|
*/
|
||||||
|
pub fn set_partially_installed(
|
||||||
|
meta: &DownloadableMetadata,
|
||||||
|
install_dir: String,
|
||||||
|
app_handle: Option<&AppHandle>,
|
||||||
|
) {
|
||||||
|
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_partially_installed_db(
|
||||||
|
db_lock: &mut Database,
|
||||||
|
meta: &DownloadableMetadata,
|
||||||
|
install_dir: String,
|
||||||
|
app_handle: Option<&AppHandle>,
|
||||||
|
) {
|
||||||
|
db_lock.applications.transient_statuses.remove(meta);
|
||||||
|
db_lock.applications.game_statuses.insert(
|
||||||
|
meta.id.clone(),
|
||||||
|
GameDownloadStatus::PartiallyInstalled {
|
||||||
|
version_name: meta.version.as_ref().unwrap().clone(),
|
||||||
|
install_dir,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
db_lock
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.insert(meta.id.clone(), meta.clone());
|
||||||
|
|
||||||
|
if let Some(app_handle) = app_handle {
|
||||||
|
push_game_update(
|
||||||
|
app_handle,
|
||||||
|
&meta.id,
|
||||||
|
None,
|
||||||
|
GameStatusManager::fetch_state(&meta.id, db_lock),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||||
|
debug!("triggered uninstall for agent");
|
||||||
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.transient_statuses
|
||||||
|
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
||||||
|
|
||||||
|
push_game_update(
|
||||||
|
app_handle,
|
||||||
|
&meta.id,
|
||||||
|
None,
|
||||||
|
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||||
|
);
|
||||||
|
|
||||||
|
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||||
|
|
||||||
|
let previous_state = if let Some(state) = previous_state {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
warn!("uninstall job doesn't have previous state, failing silently");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((_, install_dir)) = match previous_state {
|
||||||
|
GameDownloadStatus::Installed {
|
||||||
|
version_name,
|
||||||
|
install_dir,
|
||||||
|
} => Some((version_name, install_dir)),
|
||||||
|
GameDownloadStatus::SetupRequired {
|
||||||
|
version_name,
|
||||||
|
install_dir,
|
||||||
|
} => Some((version_name, install_dir)),
|
||||||
|
GameDownloadStatus::PartiallyInstalled {
|
||||||
|
version_name,
|
||||||
|
install_dir,
|
||||||
|
} => Some((version_name, install_dir)),
|
||||||
|
_ => None,
|
||||||
|
} {
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.transient_statuses
|
||||||
|
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
||||||
|
|
||||||
|
drop(db_handle);
|
||||||
|
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
spawn(move || {
|
||||||
|
if let Err(e) = remove_dir_all(install_dir) {
|
||||||
|
error!("{e}");
|
||||||
|
} else {
|
||||||
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
db_handle.applications.transient_statuses.remove(&meta);
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.remove(&meta.id);
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.game_statuses
|
||||||
|
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
|
||||||
|
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||||
|
|
||||||
|
push_game_update(
|
||||||
|
&app_handle,
|
||||||
|
&meta.id,
|
||||||
|
None,
|
||||||
|
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!("uninstalled game id {}", &meta.id);
|
||||||
|
app_emit!(&app_handle, "update_library", ());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
warn!("invalid previous state for uninstall, failing silently.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||||
|
borrow_db_checked()
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.get(game_id)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_game_complete(
|
||||||
|
meta: &DownloadableMetadata,
|
||||||
|
install_dir: String,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
) -> Result<(), RemoteAccessError> {
|
||||||
|
// Fetch game version information from remote
|
||||||
|
if meta.version.is_none() {
|
||||||
|
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = generate_url(
|
||||||
|
&["/api/v1/client/game/version"],
|
||||||
|
&[
|
||||||
|
("id", &meta.id),
|
||||||
|
("version", meta.version.as_ref().unwrap()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
let response = client
|
||||||
|
.get(response)
|
||||||
|
.header("Authorization", generate_authorization_header())
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let game_version: GameVersion = response.json()?;
|
||||||
|
|
||||||
|
let mut handle = borrow_db_mut_checked();
|
||||||
|
handle
|
||||||
|
.applications
|
||||||
|
.game_versions
|
||||||
|
.entry(meta.id.clone())
|
||||||
|
.or_default()
|
||||||
|
.insert(meta.version.clone().unwrap(), game_version.clone());
|
||||||
|
handle
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.insert(meta.id.clone(), meta.clone());
|
||||||
|
|
||||||
|
drop(handle);
|
||||||
|
|
||||||
|
let status = if game_version.setup_command.is_empty() {
|
||||||
|
GameDownloadStatus::Installed {
|
||||||
|
version_name: meta.version.clone().unwrap(),
|
||||||
|
install_dir,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GameDownloadStatus::SetupRequired {
|
||||||
|
version_name: meta.version.clone().unwrap(),
|
||||||
|
install_dir,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.game_statuses
|
||||||
|
.insert(meta.id.clone(), status.clone());
|
||||||
|
drop(db_handle);
|
||||||
|
app_emit!(
|
||||||
|
app_handle,
|
||||||
|
&format!("update_game/{}", meta.id),
|
||||||
|
GameUpdateEvent {
|
||||||
|
game_id: meta.id.clone(),
|
||||||
|
status: (Some(status), None),
|
||||||
|
version: Some(game_version),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_game_update(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
game_id: &String,
|
||||||
|
version: Option<GameVersion>,
|
||||||
|
status: GameStatusWithTransient,
|
||||||
|
) {
|
||||||
|
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
||||||
|
&status.0
|
||||||
|
&& version.is_none()
|
||||||
|
{
|
||||||
|
panic!("pushed game for installed game that doesn't have version information");
|
||||||
|
}
|
||||||
|
|
||||||
|
app_emit!(
|
||||||
|
app_handle,
|
||||||
|
&format!("update_game/{game_id}"),
|
||||||
|
GameUpdateEvent {
|
||||||
|
game_id: game_id.clone(),
|
||||||
|
status,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FrontendGameOptions {
|
||||||
|
launch_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrontendGameOptions {
|
||||||
|
pub fn launch_string(&self) -> &String {
|
||||||
|
&self.launch_string
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,11 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
use database::{DownloadType, DownloadableMetadata, borrow_db_mut_checked};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{
|
downloads::drop_data::{DROP_DATA_PATH, DropData},
|
||||||
db::borrow_db_mut_checked,
|
library::set_partially_installed_db,
|
||||||
models::data::v1::{DownloadType, DownloadableMetadata},
|
|
||||||
},
|
|
||||||
games::{
|
|
||||||
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
|
|
||||||
library::set_partially_installed_db,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn scan_install_dirs() {
|
pub fn scan_install_dirs() {
|
||||||
@ -24,11 +19,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;
|
||||||
};
|
};
|
||||||
@ -1,4 +1,6 @@
|
|||||||
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus};
|
use 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() {
|
||||||
19
src-tauri/process/Cargo.toml
Normal file
19
src-tauri/process/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "process"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.42"
|
||||||
|
client = { version = "0.1.0", path = "../client" }
|
||||||
|
database = { version = "0.1.0", path = "../database" }
|
||||||
|
dynfmt = "0.1.5"
|
||||||
|
games = { version = "0.1.0", path = "../games" }
|
||||||
|
log = "0.4.28"
|
||||||
|
page_size = "0.6.0"
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_with = "3.15.0"
|
||||||
|
shared_child = "1.1.1"
|
||||||
|
tauri = "2.8.5"
|
||||||
|
tauri-plugin-opener = "2.5.0"
|
||||||
|
utils = { version = "0.1.0", path = "../utils" }
|
||||||
@ -11,7 +11,9 @@ 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),
|
||||||
|
FailedLaunch(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ProcessError {
|
impl Display for ProcessError {
|
||||||
@ -23,9 +25,15 @@ 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(error) => &format!("Could not format template: {error:?}"),
|
||||||
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}")
|
||||||
|
}
|
||||||
|
ProcessError::FailedLaunch(game_id) => {
|
||||||
|
&format!("Drop detected that the game {game_id} may have failed to launch properly")
|
||||||
|
}
|
||||||
|
};
|
||||||
write!(f, "{s}")
|
write!(f, "{s}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,7 +8,12 @@ pub struct DropFormatArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DropFormatArgs {
|
impl DropFormatArgs {
|
||||||
pub fn new(launch_string: String, working_dir: &String, executable_name: &String, absolute_executable_name: String) -> Self {
|
pub fn new(
|
||||||
|
launch_string: String,
|
||||||
|
working_dir: &String,
|
||||||
|
executable_name: &String,
|
||||||
|
absolute_executable_name: String,
|
||||||
|
) -> Self {
|
||||||
let mut positional = Vec::new();
|
let mut positional = Vec::new();
|
||||||
let mut map: HashMap<&'static str, String> = HashMap::new();
|
let mut map: HashMap<&'static str, String> = HashMap::new();
|
||||||
|
|
||||||
41
src-tauri/process/src/lib.rs
Normal file
41
src-tauri/process/src/lib.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#![feature(nonpoison_mutex)]
|
||||||
|
#![feature(sync_nonpoison)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
ops::Deref,
|
||||||
|
sync::{OnceLock, nonpoison::Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
use crate::process_manager::ProcessManager;
|
||||||
|
|
||||||
|
pub static PROCESS_MANAGER: ProcessManagerWrapper = ProcessManagerWrapper::new();
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod format;
|
||||||
|
pub mod process_handlers;
|
||||||
|
pub mod process_manager;
|
||||||
|
|
||||||
|
pub struct ProcessManagerWrapper(OnceLock<Mutex<ProcessManager<'static>>>);
|
||||||
|
impl ProcessManagerWrapper {
|
||||||
|
const fn new() -> Self {
|
||||||
|
ProcessManagerWrapper(OnceLock::new())
|
||||||
|
}
|
||||||
|
pub fn init(app_handle: AppHandle) {
|
||||||
|
PROCESS_MANAGER
|
||||||
|
.0
|
||||||
|
.set(Mutex::new(ProcessManager::new(app_handle)))
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to initialise Process Manager")); // Using panic! here because we can't implement Debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for ProcessManagerWrapper {
|
||||||
|
type Target = Mutex<ProcessManager<'static>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self.0.get() {
|
||||||
|
Some(process_manager) => process_manager,
|
||||||
|
None => unreachable!("Download manager should always be initialised"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,8 @@
|
|||||||
use std::{
|
use client::compat::{COMPAT_INFO, UMU_LAUNCHER_EXECUTABLE};
|
||||||
ffi::OsStr,
|
use database::{Database, DownloadableMetadata, GameVersion, platform::Platform};
|
||||||
path::PathBuf,
|
use log::debug;
|
||||||
process::{Command, Stdio},
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::{debug, info};
|
use crate::{error::ProcessError, process_manager::ProcessHandler};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
AppState,
|
|
||||||
database::models::data::{Database, DownloadableMetadata, GameVersion},
|
|
||||||
process::process_manager::{Platform, ProcessHandler},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct NativeGameLauncher;
|
pub struct NativeGameLauncher;
|
||||||
impl ProcessHandler for NativeGameLauncher {
|
impl ProcessHandler for NativeGameLauncher {
|
||||||
@ -22,40 +13,15 @@ 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, _target: &Platform) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
|
|
||||||
let x = get_umu_executable();
|
|
||||||
info!("{:?}", &x);
|
|
||||||
x
|
|
||||||
});
|
|
||||||
const UMU_BASE_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
|
||||||
const UMU_INSTALL_DIRS: [&str; 4] = ["/app/share", "/use/local/share", "/usr/share", "/opt"];
|
|
||||||
|
|
||||||
fn get_umu_executable() -> Option<PathBuf> {
|
|
||||||
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
|
|
||||||
return Some(PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
for dir in UMU_INSTALL_DIRS {
|
|
||||||
let p = PathBuf::from(dir).join(UMU_BASE_LAUNCHER_EXECUTABLE);
|
|
||||||
if check_executable_exists(&p) {
|
|
||||||
return Some(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
|
|
||||||
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).output();
|
|
||||||
has_umu_installed.is_ok()
|
|
||||||
}
|
|
||||||
pub struct UMULauncher;
|
pub struct UMULauncher;
|
||||||
impl ProcessHandler for UMULauncher {
|
impl ProcessHandler for UMULauncher {
|
||||||
fn create_launch_process(
|
fn create_launch_process(
|
||||||
@ -65,7 +31,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,16 +43,18 @@ 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, _target: &Platform) -> bool {
|
||||||
let Some(ref compat_info) = state.compat_info else {
|
let Some(compat_info) = &*COMPAT_INFO else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
compat_info.umu_installed
|
compat_info.umu_installed
|
||||||
@ -102,7 +70,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,20 +78,28 @@ 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)]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@ -135,7 +111,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ref compat_info) = state.compat_info else {
|
let Some(compat_info) = &*COMPAT_INFO else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,38 +1,31 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::{OpenOptions, create_dir_all},
|
fs::{OpenOptions, create_dir_all},
|
||||||
io::{self},
|
io,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Command, ExitStatus},
|
process::{Command, ExitStatus},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, Mutex},
|
sync::Arc,
|
||||||
thread::spawn,
|
thread::spawn,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use database::{
|
||||||
|
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||||
|
GameVersion, borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, platform::Platform,
|
||||||
|
};
|
||||||
use dynfmt::Format;
|
use dynfmt::Format;
|
||||||
use dynfmt::SimpleCurlyFormat;
|
use dynfmt::SimpleCurlyFormat;
|
||||||
|
use games::{library::push_game_update, state::GameStatusManager};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use shared_child::SharedChild;
|
use shared_child::SharedChild;
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, DB,
|
PROCESS_MANAGER,
|
||||||
database::{
|
error::ProcessError,
|
||||||
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
|
format::DropFormatArgs,
|
||||||
models::data::{
|
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
||||||
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
|
|
||||||
GameDownloadStatus, GameVersion,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error::process_error::ProcessError,
|
|
||||||
games::{library::push_game_update, state::GameStatusManager},
|
|
||||||
process::{
|
|
||||||
format::DropFormatArgs,
|
|
||||||
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct RunningProcess {
|
pub struct RunningProcess {
|
||||||
@ -45,11 +38,11 @@ pub struct ProcessManager<'a> {
|
|||||||
current_platform: Platform,
|
current_platform: Platform,
|
||||||
log_output_dir: PathBuf,
|
log_output_dir: PathBuf,
|
||||||
processes: HashMap<String, RunningProcess>,
|
processes: HashMap<String, RunningProcess>,
|
||||||
app_handle: AppHandle,
|
|
||||||
game_launchers: Vec<(
|
game_launchers: Vec<(
|
||||||
(Platform, Platform),
|
(Platform, Platform),
|
||||||
&'a (dyn ProcessHandler + Sync + Send + 'static),
|
&'a (dyn ProcessHandler + Sync + Send + 'static),
|
||||||
)>,
|
)>,
|
||||||
|
app_handle: AppHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessManager<'_> {
|
impl ProcessManager<'_> {
|
||||||
@ -61,12 +54,11 @@ impl ProcessManager<'_> {
|
|||||||
current_platform: Platform::Windows,
|
current_platform: Platform::Windows,
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
current_platform: Platform::MacOs,
|
current_platform: Platform::macOS,
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
current_platform: Platform::Linux,
|
current_platform: Platform::Linux,
|
||||||
|
|
||||||
app_handle,
|
|
||||||
processes: HashMap::new(),
|
processes: HashMap::new(),
|
||||||
log_output_dir,
|
log_output_dir,
|
||||||
game_launchers: vec![
|
game_launchers: vec![
|
||||||
@ -80,7 +72,7 @@ impl ProcessManager<'_> {
|
|||||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
(Platform::MacOs, Platform::MacOs),
|
(Platform::macOS, Platform::macOS),
|
||||||
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -92,6 +84,7 @@ impl ProcessManager<'_> {
|
|||||||
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
app_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,30 +103,31 @@ impl ProcessManager<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_log_dir(&self, game_id: String) -> PathBuf {
|
pub fn get_log_dir(&self, game_id: String) -> PathBuf {
|
||||||
self.log_output_dir.join(game_id)
|
self.log_output_dir.join(game_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> {
|
fn on_process_finish(
|
||||||
let dir = self.get_log_dir(game_id);
|
&mut self,
|
||||||
self.app_handle
|
game_id: String,
|
||||||
.opener()
|
result: Result<ExitStatus, std::io::Error>,
|
||||||
.open_path(dir.to_str().unwrap(), None::<&str>)
|
) -> Result<(), ProcessError> {
|
||||||
.map_err(ProcessError::OpenerError)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
|
|
||||||
if !self.processes.contains_key(&game_id) {
|
if !self.processes.contains_key(&game_id) {
|
||||||
warn!(
|
warn!(
|
||||||
"process on_finish was called, but game_id is no longer valid. finished with result: {result:?}"
|
"process on_finish was called, but game_id is no longer valid. finished with result: {result:?}"
|
||||||
);
|
);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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 +135,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 +160,18 @@ 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);
|
return Err(ProcessError::FailedLaunch(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);
|
||||||
|
|
||||||
@ -189,12 +181,12 @@ impl ProcessManager<'_> {
|
|||||||
Some(version_data.clone()),
|
Some(version_data.clone()),
|
||||||
status,
|
status,
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_process_handler(
|
fn fetch_process_handler(
|
||||||
&self,
|
&self,
|
||||||
db_lock: &Database,
|
db_lock: &Database,
|
||||||
state: &AppState,
|
|
||||||
target_platform: &Platform,
|
target_platform: &Platform,
|
||||||
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
|
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
|
||||||
Ok(self
|
Ok(self
|
||||||
@ -204,30 +196,25 @@ impl ProcessManager<'_> {
|
|||||||
let (e_current, e_target) = e.0;
|
let (e_current, e_target) = e.0;
|
||||||
e_current == self.current_platform
|
e_current == self.current_platform
|
||||||
&& e_target == *target_platform
|
&& e_target == *target_platform
|
||||||
&& e.1.valid_for_platform(db_lock, state, target_platform)
|
&& e.1.valid_for_platform(db_lock, target_platform)
|
||||||
})
|
})
|
||||||
.ok_or(ProcessError::InvalidPlatform)?
|
.ok_or(ProcessError::InvalidPlatform)?
|
||||||
.1)
|
.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
|
pub fn valid_platform(&self, platform: &Platform) -> 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, platform);
|
||||||
Ok(process_handler.is_ok())
|
process_handler.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch_process(
|
/// Must be called through spawn as it is currently blocking
|
||||||
&mut self,
|
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
|
||||||
game_id: String,
|
|
||||||
state: &AppState,
|
|
||||||
) -> Result<(), ProcessError> {
|
|
||||||
if self.processes.contains_key(&game_id) {
|
if self.processes.contains_key(&game_id) {
|
||||||
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 +253,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
|
||||||
@ -304,7 +291,7 @@ impl ProcessManager<'_> {
|
|||||||
|
|
||||||
let target_platform = game_version.platform;
|
let target_platform = game_version.platform;
|
||||||
|
|
||||||
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
|
let process_handler = self.fetch_process_handler(&db_lock, &target_platform)?;
|
||||||
|
|
||||||
let (launch, args) = match game_status {
|
let (launch, args) = match game_status {
|
||||||
GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
@ -322,8 +309,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 +319,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,
|
||||||
@ -385,24 +373,8 @@ impl ProcessManager<'_> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let wait_thread_handle = launch_process_handle.clone();
|
let wait_thread_handle = launch_process_handle.clone();
|
||||||
let wait_thread_apphandle = self.app_handle.clone();
|
|
||||||
let wait_thread_game_id = meta.clone();
|
let wait_thread_game_id = meta.clone();
|
||||||
|
|
||||||
spawn(move || {
|
|
||||||
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
|
||||||
|
|
||||||
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
|
||||||
let app_state_handle = app_state.lock().unwrap();
|
|
||||||
|
|
||||||
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
|
||||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
|
||||||
|
|
||||||
// As everything goes out of scope, they should get dropped
|
|
||||||
// But just to explicit about it
|
|
||||||
drop(process_manager_handle);
|
|
||||||
drop(app_state_handle);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.processes.insert(
|
self.processes.insert(
|
||||||
meta.id,
|
meta.id,
|
||||||
RunningProcess {
|
RunningProcess {
|
||||||
@ -411,55 +383,17 @@ impl ProcessManager<'_> {
|
|||||||
manually_killed: false,
|
manually_killed: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
spawn(move || {
|
||||||
|
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
||||||
|
|
||||||
|
PROCESS_MANAGER
|
||||||
|
.lock()
|
||||||
|
.on_process_finish(wait_thread_game_id.id, result)
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
|
||||||
pub enum Platform {
|
|
||||||
Windows,
|
|
||||||
Linux,
|
|
||||||
MacOs,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Platform {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub const HOST: Platform = Self::Windows;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub const HOST: Platform = Self::MacOs;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub const HOST: Platform = Self::Linux;
|
|
||||||
|
|
||||||
pub fn is_case_sensitive(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Windows | Self::MacOs => false,
|
|
||||||
Self::Linux => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for Platform {
|
|
||||||
fn from(value: &str) -> Self {
|
|
||||||
match value.to_lowercase().trim() {
|
|
||||||
"windows" => Self::Windows,
|
|
||||||
"linux" => Self::Linux,
|
|
||||||
"mac" | "macos" => Self::MacOs,
|
|
||||||
_ => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<whoami::Platform> for Platform {
|
|
||||||
fn from(value: whoami::Platform) -> Self {
|
|
||||||
match value {
|
|
||||||
whoami::Platform::Windows => Platform::Windows,
|
|
||||||
whoami::Platform::Linux => Platform::Linux,
|
|
||||||
whoami::Platform::MacOS => Platform::MacOs,
|
|
||||||
_ => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ProcessHandler: Send + 'static {
|
pub trait ProcessHandler: Send + 'static {
|
||||||
fn create_launch_process(
|
fn create_launch_process(
|
||||||
&self,
|
&self,
|
||||||
@ -468,7 +402,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, target: &Platform) -> bool;
|
||||||
}
|
}
|
||||||
23
src-tauri/remote/Cargo.toml
Normal file
23
src-tauri/remote/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "remote"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitcode = "0.6.7"
|
||||||
|
chrono = "0.4.42"
|
||||||
|
client = { version = "0.1.0", path = "../client" }
|
||||||
|
database = { version = "0.1.0", path = "../database" }
|
||||||
|
droplet-rs = "0.7.3"
|
||||||
|
gethostname = "1.0.2"
|
||||||
|
hex = "0.4.3"
|
||||||
|
http = "1.3.1"
|
||||||
|
log = "0.4.28"
|
||||||
|
md5 = "0.8.0"
|
||||||
|
reqwest = "0.12.23"
|
||||||
|
reqwest-websocket = "0.5.1"
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_with = "3.15.0"
|
||||||
|
tauri = "2.8.5"
|
||||||
|
url = "2.5.7"
|
||||||
|
utils = { version = "0.1.0", path = "../utils" }
|
||||||
152
src-tauri/remote/src/auth.rs
Normal file
152
src-tauri/remote/src/auth.rs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
use std::{collections::HashMap, env};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use client::{app_status::AppStatus, user::User};
|
||||||
|
use database::{DatabaseAuth, interface::borrow_db_checked};
|
||||||
|
use droplet_rs::ssl::sign_nonce;
|
||||||
|
use gethostname::gethostname;
|
||||||
|
use log::{error, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{DropServerError, RemoteAccessError},
|
||||||
|
requests::make_authenticated_get,
|
||||||
|
utils::DROP_CLIENT_SYNC,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
cache::{cache_object, get_cached_object},
|
||||||
|
requests::generate_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CapabilityConfiguration {}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct InitiateRequestBody {
|
||||||
|
name: String,
|
||||||
|
platform: String,
|
||||||
|
capabilities: HashMap<String, CapabilityConfiguration>,
|
||||||
|
mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HandshakeRequestBody {
|
||||||
|
client_id: String,
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HandshakeRequestBody {
|
||||||
|
pub fn new(client_id: String, token: String) -> Self {
|
||||||
|
Self { client_id, token }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HandshakeResponse {
|
||||||
|
private: String,
|
||||||
|
certificate: String,
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HandshakeResponse> for DatabaseAuth {
|
||||||
|
fn from(value: HandshakeResponse) -> Self {
|
||||||
|
DatabaseAuth::new(value.private, value.certificate, value.id, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_authorization_header() -> String {
|
||||||
|
let certs = {
|
||||||
|
let db = borrow_db_checked();
|
||||||
|
db.auth.clone().expect("Authorisation not initialised")
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = Utc::now().timestamp_millis().to_string();
|
||||||
|
|
||||||
|
let signature =
|
||||||
|
sign_nonce(certs.private, nonce.clone()).expect("Failed to generate authorisation header");
|
||||||
|
|
||||||
|
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||||
|
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
|
||||||
|
if response.status() != 200 {
|
||||||
|
let err: DropServerError = response.json().await?;
|
||||||
|
warn!("{err:?}");
|
||||||
|
|
||||||
|
if err.status_message == "Nonce expired" {
|
||||||
|
return Err(RemoteAccessError::OutOfSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
.json::<User>()
|
||||||
|
.await
|
||||||
|
.map_err(std::convert::Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
||||||
|
let base_url = {
|
||||||
|
let db_lock = borrow_db_checked();
|
||||||
|
Url::parse(&db_lock.base_url.clone())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let hostname = gethostname();
|
||||||
|
|
||||||
|
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
||||||
|
let body = InitiateRequestBody {
|
||||||
|
name: format!("{} (Desktop)", hostname.display()),
|
||||||
|
platform: env::consts::OS.to_string(),
|
||||||
|
capabilities: HashMap::from([
|
||||||
|
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||||
|
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
||||||
|
]),
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||||
|
|
||||||
|
if response.status() != 200 {
|
||||||
|
let data: DropServerError = response.json()?;
|
||||||
|
error!("could not start handshake: {}", data.status_message);
|
||||||
|
|
||||||
|
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = response.text()?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup() -> (AppStatus, Option<User>) {
|
||||||
|
let auth = {
|
||||||
|
let data = borrow_db_checked();
|
||||||
|
data.auth.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if auth.is_some() {
|
||||||
|
let user_result = match fetch_user().await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(RemoteAccessError::FetchError(_)) => {
|
||||||
|
let user = get_cached_object::<User>("user").ok();
|
||||||
|
return (AppStatus::Offline, user);
|
||||||
|
}
|
||||||
|
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||||
|
};
|
||||||
|
if let Err(e) = cache_object("user", &user_result) {
|
||||||
|
warn!("Could not cache user object with error {e}");
|
||||||
|
}
|
||||||
|
return (AppStatus::SignedIn, Some(user_result));
|
||||||
|
}
|
||||||
|
|
||||||
|
(AppStatus::SignedOut, None)
|
||||||
|
}
|
||||||
@ -5,18 +5,19 @@ use std::{
|
|||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::{db::borrow_db_checked, models::data::Database},
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
};
|
|
||||||
use bitcode::{Decode, DecodeOwned, Encode};
|
use bitcode::{Decode, DecodeOwned, Encode};
|
||||||
|
use database::{Database, borrow_db_checked};
|
||||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||||
|
|
||||||
|
use crate::error::{CacheError, RemoteAccessError};
|
||||||
|
|
||||||
#[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 ::database::borrow_db_checked().settings.force_offline
|
||||||
|
|| $var.lock().status == ::client::app_status::AppStatus::Offline {
|
||||||
$func2( $( $arg ), *).await
|
$func2( $( $arg ), *).await
|
||||||
} else {
|
} else {
|
||||||
$func1( $( $arg ), *).await
|
$func1( $( $arg ), *).await
|
||||||
@ -50,6 +51,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 +80,14 @@ 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 +102,39 @@ 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 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);
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,11 +4,20 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use http::StatusCode;
|
use http::{HeaderName, StatusCode, header::ToStrError};
|
||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
use url::ParseError;
|
use url::ParseError;
|
||||||
|
|
||||||
use super::drop_server_error::DropServerError;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DropServerError {
|
||||||
|
pub status_code: usize,
|
||||||
|
pub status_message: String,
|
||||||
|
// pub message: String,
|
||||||
|
// pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum RemoteAccessError {
|
pub enum RemoteAccessError {
|
||||||
@ -44,8 +53,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 +62,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}")
|
||||||
@ -106,3 +113,31 @@ impl From<ParseError> for RemoteAccessError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl std::error::Error for RemoteAccessError {}
|
impl std::error::Error for 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src-tauri/remote/src/fetch_object.rs
Normal file
82
src-tauri/remote/src/fetch_object.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use database::{DB, interface::DatabaseImpls};
|
||||||
|
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||||
|
use log::{debug, warn};
|
||||||
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
|
use crate::{error::CacheError, utils::DROP_CLIENT_ASYNC};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
auth::generate_authorization_header,
|
||||||
|
cache::{ObjectCache, cache_object, get_cached_object},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 /
|
||||||
|
let object_id = &request.uri().path()[1..];
|
||||||
|
|
||||||
|
let cache_result = get_cached_object::<ObjectCache>(object_id);
|
||||||
|
if let Ok(cache_result) = &cache_result
|
||||||
|
&& !cache_result.has_expired()
|
||||||
|
{
|
||||||
|
return cache_result.try_into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = generate_authorization_header();
|
||||||
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
|
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
|
||||||
|
let response = client.get(url).header("Authorization", header).send().await;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(r) => {
|
||||||
|
let resp_builder = ResponseBuilder::new().header(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
r.headers()
|
||||||
|
.get("Content-Type")
|
||||||
|
.expect("Failed get Content-Type header"),
|
||||||
|
);
|
||||||
|
let data = match r.bytes().await {
|
||||||
|
Ok(data) => Vec::from(data),
|
||||||
|
Err(e) => {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod commands;
|
pub mod error;
|
||||||
pub mod fetch_object;
|
pub mod fetch_object;
|
||||||
pub mod requests;
|
pub mod requests;
|
||||||
pub mod server_proto;
|
pub mod server_proto;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use auth::setup;
|
||||||
@ -1,10 +1,8 @@
|
|||||||
|
use database::{DB, interface::DatabaseImpls};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
DB,
|
auth::generate_authorization_header, error::RemoteAccessError, utils::DROP_CLIENT_ASYNC,
|
||||||
database::db::DatabaseImpls,
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
remote::{auth::generate_authorization_header, utils::DROP_CLIENT_ASYNC},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn generate_url<T: AsRef<str>>(
|
pub fn generate_url<T: AsRef<str>>(
|
||||||
108
src-tauri/remote/src/server_proto.rs
Normal file
108
src-tauri/remote/src/server_proto.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use database::borrow_db_checked;
|
||||||
|
use http::{Request, Response, StatusCode, Uri, uri::PathAndQuery};
|
||||||
|
use log::{error, warn};
|
||||||
|
use tauri::UriSchemeResponder;
|
||||||
|
use utils::webbrowser_open::webbrowser_open;
|
||||||
|
|
||||||
|
use crate::utils::DROP_CLIENT_SYNC;
|
||||||
|
|
||||||
|
pub async fn handle_server_proto_offline_wrapper(
|
||||||
|
request: Request<Vec<u8>>,
|
||||||
|
responder: UriSchemeResponder,
|
||||||
|
) {
|
||||||
|
responder.respond(match handle_server_proto_offline(request).await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => unreachable!(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 auth = match db_handle.auth.as_ref() {
|
||||||
|
Some(auth) => auth,
|
||||||
|
None => {
|
||||||
|
error!("Could not find auth in database");
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let web_token = match &auth.web_token {
|
||||||
|
Some(token) => token,
|
||||||
|
None => return Err(StatusCode::UNAUTHORIZED),
|
||||||
|
};
|
||||||
|
let remote_uri = db_handle
|
||||||
|
.base_url
|
||||||
|
.parse::<Uri>()
|
||||||
|
.expect("Failed to parse base url");
|
||||||
|
|
||||||
|
let path = request.uri().path();
|
||||||
|
|
||||||
|
let mut new_uri = request.uri().clone().into_parts();
|
||||||
|
new_uri.path_and_query = Some(
|
||||||
|
PathAndQuery::from_str(&format!("{path}?noWrapper=true"))
|
||||||
|
.expect("Failed to parse request path in proto"),
|
||||||
|
);
|
||||||
|
new_uri.authority = remote_uri.authority().cloned();
|
||||||
|
new_uri.scheme = remote_uri.scheme().cloned();
|
||||||
|
let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
|
||||||
|
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
|
||||||
|
|
||||||
|
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
|
||||||
|
|
||||||
|
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
|
||||||
|
webbrowser_open(new_uri.to_string());
|
||||||
|
return Ok(Response::new(Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = match client
|
||||||
|
.request(request.method().clone(), new_uri.to_string())
|
||||||
|
.header("Authorization", format!("Bearer {web_token}"))
|
||||||
|
.headers(request.headers().clone())
|
||||||
|
.send()
|
||||||
|
{
|
||||||
|
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_body = match response.bytes() {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let http_response = Response::builder()
|
||||||
|
.status(response_status)
|
||||||
|
.body(response_body.to_vec())
|
||||||
|
.expect("Failed to build server proto response");
|
||||||
|
|
||||||
|
Ok(http_response)
|
||||||
|
}
|
||||||
119
src-tauri/remote/src/utils.rs
Normal file
119
src-tauri/remote/src/utils.rs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::Read,
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use database::db::DATA_ROOT_DIR;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use reqwest::Certificate;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DropHealthcheck {
|
||||||
|
app_name: String,
|
||||||
|
}
|
||||||
|
impl DropHealthcheck {
|
||||||
|
pub fn app_name(&self) -> &String {
|
||||||
|
&self.app_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
|
||||||
|
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
|
||||||
|
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
|
||||||
|
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
|
||||||
|
|
||||||
|
fn fetch_certificates() -> Vec<Certificate> {
|
||||||
|
let certificate_dir = DATA_ROOT_DIR.join("certificates");
|
||||||
|
|
||||||
|
let mut certs = Vec::new();
|
||||||
|
match fs::read_dir(certificate_dir) {
|
||||||
|
Ok(c) => {
|
||||||
|
for entry in c {
|
||||||
|
match entry {
|
||||||
|
Ok(c) => {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
match File::open(c.path()) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to open file at {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.read_to_end(&mut buf)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"Failed to read to end of certificate file {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
match Certificate::from_pem_bundle(&buf) {
|
||||||
|
Ok(certificates) => {
|
||||||
|
for cert in certificates {
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"added {} certificate(s) from {}",
|
||||||
|
certs.len(),
|
||||||
|
c.file_name().display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => warn!(
|
||||||
|
"Invalid certificate file {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("not loading certificates due to error: {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
certs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_sync() -> reqwest::blocking::Client {
|
||||||
|
let mut client = reqwest::blocking::ClientBuilder::new();
|
||||||
|
|
||||||
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
|
client = client.add_root_certificate(cert.clone());
|
||||||
|
}
|
||||||
|
client
|
||||||
|
.use_rustls_tls()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build synchronous client")
|
||||||
|
}
|
||||||
|
pub fn get_client_async() -> reqwest::Client {
|
||||||
|
let mut client = reqwest::ClientBuilder::new();
|
||||||
|
|
||||||
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
|
client = client.add_root_certificate(cert.clone());
|
||||||
|
}
|
||||||
|
client
|
||||||
|
.use_rustls_tls()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build asynchronous client")
|
||||||
|
}
|
||||||
|
pub fn get_client_ws() -> reqwest::Client {
|
||||||
|
let mut client = reqwest::ClientBuilder::new();
|
||||||
|
|
||||||
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
|
client = client.add_root_certificate(cert.clone());
|
||||||
|
}
|
||||||
|
client
|
||||||
|
.use_rustls_tls()
|
||||||
|
.http1_only()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build websocket client")
|
||||||
|
}
|
||||||
82
src-tauri/src/client.rs
Normal file
82
src-tauri/src/client.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use std::sync::nonpoison::Mutex;
|
||||||
|
|
||||||
|
use database::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
|
use download_manager::DOWNLOAD_MANAGER;
|
||||||
|
use log::{debug, error};
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn fetch_state(state: tauri::State<'_, Mutex<AppState>>) -> Result<String, String> {
|
||||||
|
let guard = state.lock();
|
||||||
|
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||||
|
drop(guard);
|
||||||
|
Ok(cloned_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn quit(app: tauri::AppHandle) {
|
||||||
|
cleanup_and_exit(&app);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_and_exit(app: &AppHandle) {
|
||||||
|
debug!("cleaning up and exiting application");
|
||||||
|
match DOWNLOAD_MANAGER.ensure_terminated() {
|
||||||
|
Ok(res) => match res {
|
||||||
|
Ok(()) => debug!("download manager terminated correctly"),
|
||||||
|
Err(()) => error!("download manager failed to terminate correctly"),
|
||||||
|
},
|
||||||
|
Err(e) => panic!("{e:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
|
let manager = app.autolaunch();
|
||||||
|
if enabled {
|
||||||
|
manager.enable().map_err(|e| e.to_string())?;
|
||||||
|
debug!("enabled autostart");
|
||||||
|
} else {
|
||||||
|
manager.disable().map_err(|e| e.to_string())?;
|
||||||
|
debug!("eisabled autostart");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the state in DB
|
||||||
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
db_handle.settings.autostart = enabled;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||||
|
let db_handle = borrow_db_checked();
|
||||||
|
let db_state = db_handle.settings.autostart;
|
||||||
|
drop(db_handle);
|
||||||
|
|
||||||
|
// Get actual system state
|
||||||
|
let manager = app.autolaunch();
|
||||||
|
let system_state = manager.is_enabled()?;
|
||||||
|
|
||||||
|
// If they don't match, sync to DB state
|
||||||
|
if db_state != system_state {
|
||||||
|
if db_state {
|
||||||
|
manager.enable()?;
|
||||||
|
} else {
|
||||||
|
manager.disable()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(db_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_fs(path: String, app_handle: AppHandle) -> Result<(), tauri_plugin_opener::Error> {
|
||||||
|
app_handle
|
||||||
|
.opener()
|
||||||
|
.open_path(path, None::<&str>)
|
||||||
|
}
|
||||||
@ -1,75 +0,0 @@
|
|||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
|
||||||
use log::debug;
|
|
||||||
use tauri::AppHandle;
|
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
|
||||||
|
|
||||||
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|
||||||
let manager = app.autolaunch();
|
|
||||||
if enabled {
|
|
||||||
manager.enable().map_err(|e| e.to_string())?;
|
|
||||||
debug!("enabled autostart");
|
|
||||||
} else {
|
|
||||||
manager.disable().map_err(|e| e.to_string())?;
|
|
||||||
debug!("eisabled autostart");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the state in DB
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
|
||||||
db_handle.settings.autostart = enabled;
|
|
||||||
drop(db_handle);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
|
||||||
// First check DB state
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
let db_state = db_handle.settings.autostart;
|
|
||||||
drop(db_handle);
|
|
||||||
|
|
||||||
// Get actual system state
|
|
||||||
let manager = app.autolaunch();
|
|
||||||
let system_state = manager.is_enabled()?;
|
|
||||||
|
|
||||||
// If they don't match, sync to DB state
|
|
||||||
if db_state != system_state {
|
|
||||||
if db_state {
|
|
||||||
manager.enable()?;
|
|
||||||
} else {
|
|
||||||
manager.disable()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(db_state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New function to sync state on startup
|
|
||||||
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
let should_be_enabled = db_handle.settings.autostart;
|
|
||||||
drop(db_handle);
|
|
||||||
|
|
||||||
let manager = app.autolaunch();
|
|
||||||
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
if current_state != should_be_enabled {
|
|
||||||
if should_be_enabled {
|
|
||||||
manager.enable().map_err(|e| e.to_string())?;
|
|
||||||
debug!("synced autostart: enabled");
|
|
||||||
} else {
|
|
||||||
manager.disable().map_err(|e| e.to_string())?;
|
|
||||||
debug!("synced autostart: disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|
||||||
toggle_autostart_logic(app, enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
|
||||||
get_autostart_enabled_logic(app)
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
use log::{debug, error};
|
|
||||||
use tauri::AppHandle;
|
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
|
||||||
cleanup_and_exit(&app, &state);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
|
||||||
debug!("cleaning up and exiting application");
|
|
||||||
let download_manager = state.lock().unwrap().download_manager.clone();
|
|
||||||
match download_manager.ensure_terminated() {
|
|
||||||
Ok(res) => match res {
|
|
||||||
Ok(()) => debug!("download manager terminated correctly"),
|
|
||||||
Err(()) => error!("download manager failed to terminate correctly"),
|
|
||||||
},
|
|
||||||
Err(e) => panic!("{e:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exit(0);
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
use crate::AppState;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn fetch_state(
|
|
||||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let guard = state.lock().unwrap();
|
|
||||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
|
||||||
drop(guard);
|
|
||||||
Ok(cloned_state)
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod autostart;
|
|
||||||
pub mod cleanup;
|
|
||||||
pub mod commands;
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
|
|
||||||
|
|
||||||
use super::path::CommonPath;
|
|
||||||
|
|
||||||
pub struct BackupManager<'a> {
|
|
||||||
pub current_platform: Platform,
|
|
||||||
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackupManager<'_> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
BackupManager {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
current_platform: Platform::Windows,
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
current_platform: Platform::MacOs,
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
current_platform: Platform::Linux,
|
|
||||||
|
|
||||||
sources: HashMap::from([
|
|
||||||
// Current platform to target platform
|
|
||||||
(
|
|
||||||
(Platform::Windows, Platform::Windows),
|
|
||||||
&WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(Platform::Linux, Platform::Linux),
|
|
||||||
&LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(Platform::MacOs, Platform::MacOs),
|
|
||||||
&MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
|
||||||
),
|
|
||||||
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait BackupHandler: Send + Sync {
|
|
||||||
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.lock().unwrap().join("games")) }
|
|
||||||
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&game.game_id).unwrap()) }
|
|
||||||
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) }
|
|
||||||
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }
|
|
||||||
fn store_user_id_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError) }
|
|
||||||
fn os_user_name_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&whoami::username()).unwrap()) }
|
|
||||||
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winAppData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppDataLow>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDocuments>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winPublic>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winProgramData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDir>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgConfig>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::new()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LinuxBackupManager {}
|
|
||||||
impl BackupHandler for LinuxBackupManager {
|
|
||||||
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Data.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct WindowsBackupManager {}
|
|
||||||
impl BackupHandler for WindowsBackupManager {
|
|
||||||
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::DataLocal.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::DataLocalLow.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(PathBuf::from_str("C:/Windows").unwrap())
|
|
||||||
}
|
|
||||||
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Document.get().ok_or(BackupError::NotFound)?)
|
|
||||||
|
|
||||||
}
|
|
||||||
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(PathBuf::from_str("C:/ProgramData").unwrap())
|
|
||||||
}
|
|
||||||
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Public.get().ok_or(BackupError::NotFound)?)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct MacBackupManager {}
|
|
||||||
impl BackupHandler for MacBackupManager {}
|
|
||||||
@ -1,22 +1,31 @@
|
|||||||
|
use games::collections::collection::{Collection, Collections};
|
||||||
|
use remote::{
|
||||||
|
auth::generate_authorization_header,
|
||||||
|
cache::{cache_object, get_cached_object},
|
||||||
|
error::RemoteAccessError,
|
||||||
|
requests::{generate_url, make_authenticated_get},
|
||||||
|
utils::DROP_CLIENT_ASYNC,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
remote::{
|
|
||||||
auth::generate_authorization_header,
|
|
||||||
requests::{generate_url, make_authenticated_get},
|
|
||||||
utils::DROP_CLIENT_ASYNC,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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 +99,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(())
|
||||||
}
|
}
|
||||||
@ -1,5 +0,0 @@
|
|||||||
pub mod commands;
|
|
||||||
pub mod db;
|
|
||||||
pub mod debug;
|
|
||||||
pub mod models;
|
|
||||||
pub mod scan;
|
|
||||||
22
src-tauri/src/download_manager.rs
Normal file
22
src-tauri/src/download_manager.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use database::DownloadableMetadata;
|
||||||
|
use download_manager::DOWNLOAD_MANAGER;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn pause_downloads() {
|
||||||
|
DOWNLOAD_MANAGER.pause_downloads();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn resume_downloads() {
|
||||||
|
DOWNLOAD_MANAGER.resume_downloads();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn move_download_in_queue(old_index: usize, new_index: usize) {
|
||||||
|
DOWNLOAD_MANAGER.rearrange(old_index, new_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn cancel_game(meta: DownloadableMetadata) {
|
||||||
|
DOWNLOAD_MANAGER.cancel(meta);
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::{database::models::data::DownloadableMetadata, AppState};
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
|
||||||
state.lock().unwrap().download_manager.pause_downloads();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
|
||||||
state.lock().unwrap().download_manager.resume_downloads();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn move_download_in_queue(
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
old_index: usize,
|
|
||||||
new_index: usize,
|
|
||||||
) {
|
|
||||||
state
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
|
||||||
.rearrange(old_index, new_index);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
|
||||||
state.lock().unwrap().download_manager.cancel(meta);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
pub mod commands;
|
|
||||||
pub mod download_manager_builder;
|
|
||||||
pub mod download_manager_frontend;
|
|
||||||
pub mod downloadable;
|
|
||||||
pub mod util;
|
|
||||||
@ -1,39 +1,31 @@
|
|||||||
use std::{
|
use std::{path::PathBuf, sync::Arc};
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
use database::{GameDownloadStatus, borrow_db_checked};
|
||||||
|
use download_manager::{
|
||||||
|
DOWNLOAD_MANAGER, downloadable::Downloadable, error::ApplicationDownloadError,
|
||||||
};
|
};
|
||||||
|
use games::downloads::download_agent::GameDownloadAgent;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
AppState,
|
|
||||||
database::{
|
|
||||||
db::borrow_db_checked,
|
|
||||||
models::data::GameDownloadStatus,
|
|
||||||
},
|
|
||||||
download_manager::downloadable::Downloadable,
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::download_agent::GameDownloadAgent;
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_game(
|
pub async fn download_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
game_version: String,
|
game_version: String,
|
||||||
install_dir: usize,
|
install_dir: usize,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<(), ApplicationDownloadError> {
|
) -> Result<(), ApplicationDownloadError> {
|
||||||
let sender = { state.lock().unwrap().download_manager.get_sender().clone() };
|
let sender = { DOWNLOAD_MANAGER.get_sender().clone() };
|
||||||
|
|
||||||
let game_download_agent =
|
let game_download_agent = GameDownloadAgent::new_from_index(
|
||||||
GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?;
|
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()
|
DOWNLOAD_MANAGER
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
|
||||||
.queue_download(game_download_agent.clone())
|
.queue_download(game_download_agent.clone())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -41,10 +33,7 @@ pub async fn download_game(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn resume_download(
|
pub async fn resume_download(game_id: String) -> Result<(), ApplicationDownloadError> {
|
||||||
game_id: String,
|
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
|
||||||
) -> Result<(), ApplicationDownloadError> {
|
|
||||||
let s = borrow_db_checked()
|
let s = borrow_db_checked()
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.game_statuses
|
||||||
@ -62,23 +51,25 @@ pub async fn resume_download(
|
|||||||
} => (version_name, install_dir),
|
} => (version_name, install_dir),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
let sender = 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
|
DOWNLOAD_MANAGER
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.download_manager
|
|
||||||
.queue_download(game_download_agent)
|
.queue_download(game_download_agent)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -1,49 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt::{Display, Formatter},
|
|
||||||
io, sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
|
||||||
use humansize::{format_size, BINARY};
|
|
||||||
|
|
||||||
use super::remote_access_error::RemoteAccessError;
|
|
||||||
|
|
||||||
// TODO: Rename / separate from downloads
|
|
||||||
#[derive(Debug, SerializeDisplay)]
|
|
||||||
pub enum ApplicationDownloadError {
|
|
||||||
NotInitialized,
|
|
||||||
Communication(RemoteAccessError),
|
|
||||||
DiskFull(u64, u64),
|
|
||||||
#[allow(dead_code)]
|
|
||||||
Checksum,
|
|
||||||
Lock,
|
|
||||||
IoError(Arc<io::Error>),
|
|
||||||
DownloadError,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ApplicationDownloadError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ApplicationDownloadError::NotInitialized => write!(f, "Download not initalized, did something go wrong?"),
|
|
||||||
ApplicationDownloadError::DiskFull(required, available) => write!(
|
|
||||||
f,
|
|
||||||
"Game requires {}, {} remaining left on disk.",
|
|
||||||
format_size(*required, BINARY),
|
|
||||||
format_size(*available, BINARY),
|
|
||||||
),
|
|
||||||
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
|
|
||||||
ApplicationDownloadError::Lock => write!(
|
|
||||||
f,
|
|
||||||
"failed to acquire lock. Something has gone very wrong internally. Please restart the application"
|
|
||||||
),
|
|
||||||
ApplicationDownloadError::Checksum => {
|
|
||||||
write!(f, "checksum failed to validate for download")
|
|
||||||
}
|
|
||||||
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
|
||||||
ApplicationDownloadError::DownloadError => write!(
|
|
||||||
f,
|
|
||||||
"Download failed. See Download Manager status for specific error"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
use std::{fmt::Display, io, sync::mpsc::SendError};
|
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
|
||||||
|
|
||||||
#[derive(SerializeDisplay)]
|
|
||||||
pub enum DownloadManagerError<T> {
|
|
||||||
IOError(io::Error),
|
|
||||||
SignalError(SendError<T>),
|
|
||||||
}
|
|
||||||
impl<T> Display for DownloadManagerError<T> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
DownloadManagerError::IOError(error) => write!(f, "{error}"),
|
|
||||||
DownloadManagerError::SignalError(send_error) => write!(f, "{send_error}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> From<SendError<T>> for DownloadManagerError<T> {
|
|
||||||
fn from(value: SendError<T>) -> Self {
|
|
||||||
DownloadManagerError::SignalError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> From<io::Error> for DownloadManagerError<T> {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
DownloadManagerError::IOError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DropServerError {
|
|
||||||
pub status_code: usize,
|
|
||||||
pub status_message: String,
|
|
||||||
// pub message: String,
|
|
||||||
// pub url: String,
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
|
||||||
|
|
||||||
#[derive(SerializeDisplay)]
|
|
||||||
pub enum LibraryError {
|
|
||||||
MetaNotFound(String),
|
|
||||||
}
|
|
||||||
impl Display for LibraryError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
LibraryError::MetaNotFound(id) => write!(
|
|
||||||
f,
|
|
||||||
"Could not locate any installed version of game ID {id} in the database"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
pub mod application_download_error;
|
|
||||||
pub mod download_manager_error;
|
|
||||||
pub mod drop_server_error;
|
|
||||||
pub mod library_error;
|
|
||||||
pub mod process_error;
|
|
||||||
pub mod remote_access_error;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user