mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-13 00:02:41 +10:00
Compare commits
1 Commits
async
...
49-bug-bro
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f75bedcae |
23
.github/workflows/clippy.yml
vendored
23
.github/workflows/clippy.yml
vendored
@ -1,23 +0,0 @@
|
|||||||
on: push
|
|
||||||
name: Clippy check
|
|
||||||
jobs:
|
|
||||||
clippy_check:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
checks: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: install dependencies (ubuntu only)
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
components: clippy
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/clippy-check@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
args: --manifest-path ./src-tauri/Cargo.toml
|
|
||||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@ -22,9 +22,9 @@ jobs:
|
|||||||
args: '--target aarch64-apple-darwin'
|
args: '--target aarch64-apple-darwin'
|
||||||
- platform: 'macos-latest' # for Intel based macs.
|
- platform: 'macos-latest' # for Intel based macs.
|
||||||
args: '--target x86_64-apple-darwin'
|
args: '--target x86_64-apple-darwin'
|
||||||
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||||
args: ''
|
args: ''
|
||||||
- platform: 'ubuntu-22.04-arm'
|
- platform: 'ubuntu-24.04-arm'
|
||||||
args: '--target aarch64-unknown-linux-gnu'
|
args: '--target aarch64-unknown-linux-gnu'
|
||||||
- platform: 'windows-latest'
|
- platform: 'windows-latest'
|
||||||
args: ''
|
args: ''
|
||||||
@ -48,11 +48,12 @@ jobs:
|
|||||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
- name: install dependencies (ubuntu only)
|
- name: install dependencies (ubuntu only)
|
||||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
|
||||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||||
|
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||||
|
|
||||||
- name: install frontend dependencies
|
- name: install frontend dependencies
|
||||||
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||||
|
|||||||
69
.github/workflows/test.yml
vendored
Normal file
69
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: 'test'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
# This can be used to automatically publish nightlies at UTC nighttime
|
||||||
|
# schedule:
|
||||||
|
# - cron: "0 2 * * *" # run at 2 AM UTC
|
||||||
|
|
||||||
|
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-tauri:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: 'macos-latest' # for Arm based macs (M1 and above).
|
||||||
|
args: '--target aarch64-apple-darwin'
|
||||||
|
- platform: 'macos-latest' # for Intel based macs.
|
||||||
|
args: '--target x86_64-apple-darwin'
|
||||||
|
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||||
|
args: ''
|
||||||
|
- platform: 'ubuntu-24.04-arm'
|
||||||
|
args: '--target aarch64-unknown-linux-gnu'
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: install Rust nightly
|
||||||
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with:
|
||||||
|
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
|
||||||
|
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||||
|
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||||
|
|
||||||
|
- name: install frontend dependencies
|
||||||
|
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||||
|
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||||
|
releaseName: 'Auto testing release'
|
||||||
|
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
args: ${{ matrix.args }}
|
||||||
@ -4,7 +4,7 @@ Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It u
|
|||||||
|
|
||||||
## Running
|
## Running
|
||||||
Before setting up the drop app, be sure that you have a server set up.
|
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)
|
The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html)
|
||||||
|
|
||||||
## Current features
|
## Current features
|
||||||
Currently supported are the following features:
|
Currently supported are the following features:
|
||||||
|
|||||||
1
app.vue
1
app.vue
@ -22,7 +22,6 @@ const router = useRouter();
|
|||||||
const state = useAppState();
|
const state = useAppState();
|
||||||
try {
|
try {
|
||||||
state.value = JSON.parse(await invoke("fetch_state"));
|
state.value = JSON.parse(await invoke("fetch_state"));
|
||||||
console.log(state.value)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("failed to parse state", e);
|
console.error("failed to parse state", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
|
||||||
<div class="inline-flex divide-x divide-zinc-900">
|
<div class="inline-flex divide-x divide-zinc-900">
|
||||||
<button type="button" @click="() => buttonActions[props.status.type]()" :class="[
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="() => buttonActions[props.status.type]()"
|
||||||
|
:class="[
|
||||||
styles[props.status.type],
|
styles[props.status.type],
|
||||||
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
showDropdown ? 'rounded-l-md' : 'rounded-md',
|
||||||
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
]">
|
]"
|
||||||
<component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
|
>
|
||||||
|
<component
|
||||||
|
:is="buttonIcons[props.status.type]"
|
||||||
|
class="-mr-0.5 size-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
{{ buttonNames[props.status.type] }}
|
{{ buttonNames[props.status.type] }}
|
||||||
</button>
|
</button>
|
||||||
<Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
|
<Menu
|
||||||
|
v-if="showDropdown"
|
||||||
|
as="div"
|
||||||
|
class="relative inline-block text-left grow"
|
||||||
|
>
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<MenuButton :class="[
|
<MenuButton
|
||||||
|
:class="[
|
||||||
styles[props.status.type],
|
styles[props.status.type],
|
||||||
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
|
||||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||||
]">
|
]"
|
||||||
|
>
|
||||||
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
<ChevronDownIcon class="size-5" aria-hidden="true" />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
<transition
|
||||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
enter-active-class="transition ease-out duration-100"
|
||||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
|
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none"
|
||||||
|
>
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<MenuItem v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<button @click="() => emit('options')" :class="[
|
<button
|
||||||
|
@click="() => emit('options')"
|
||||||
|
:class="[
|
||||||
active
|
active
|
||||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||||
: 'text-zinc-400',
|
: 'text-zinc-400',
|
||||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||||
]">
|
]"
|
||||||
|
>
|
||||||
Options
|
Options
|
||||||
<Cog6ToothIcon class="size-5" />
|
<Cog6ToothIcon class="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<button @click="() => emit('uninstall')" :class="[
|
<button
|
||||||
|
@click="() => emit('uninstall')"
|
||||||
|
:class="[
|
||||||
active
|
active
|
||||||
? 'bg-zinc-800 text-zinc-100 outline-none'
|
? 'bg-zinc-800 text-zinc-100 outline-none'
|
||||||
: 'text-zinc-400',
|
: 'text-zinc-400',
|
||||||
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
'w-full block px-4 py-2 text-sm inline-flex justify-between',
|
||||||
]">
|
]"
|
||||||
|
>
|
||||||
Uninstall
|
Uninstall
|
||||||
<TrashIcon class="size-5" />
|
<TrashIcon class="size-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -61,7 +87,6 @@ import {
|
|||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
QueueListIcon,
|
QueueListIcon,
|
||||||
StopIcon,
|
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
} from "@heroicons/vue/20/solid";
|
} from "@heroicons/vue/20/solid";
|
||||||
|
|
||||||
@ -78,14 +103,12 @@ const emit = defineEmits<{
|
|||||||
(e: "uninstall"): void;
|
(e: "uninstall"): void;
|
||||||
(e: "kill"): void;
|
(e: "kill"): void;
|
||||||
(e: "options"): void;
|
(e: "options"): void;
|
||||||
(e: "resume"): void
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showDropdown = computed(
|
const showDropdown = computed(
|
||||||
() =>
|
() =>
|
||||||
props.status.type === GameStatusEnum.Installed ||
|
props.status.type === GameStatusEnum.Installed ||
|
||||||
props.status.type === GameStatusEnum.SetupRequired ||
|
props.status.type === GameStatusEnum.SetupRequired
|
||||||
props.status.type === GameStatusEnum.PartiallyInstalled
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles: { [key in GameStatusEnum]: string } = {
|
const styles: { [key in GameStatusEnum]: string } = {
|
||||||
@ -105,8 +128,6 @@ const styles: { [key in GameStatusEnum]: string } = {
|
|||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||||
[GameStatusEnum.Running]:
|
[GameStatusEnum.Running]:
|
||||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||||
[GameStatusEnum.PartiallyInstalled]:
|
|
||||||
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonNames: { [key in GameStatusEnum]: string } = {
|
const buttonNames: { [key in GameStatusEnum]: string } = {
|
||||||
@ -118,7 +139,6 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
|
|||||||
[GameStatusEnum.Updating]: "Updating",
|
[GameStatusEnum.Updating]: "Updating",
|
||||||
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
[GameStatusEnum.Uninstalling]: "Uninstalling",
|
||||||
[GameStatusEnum.Running]: "Stop",
|
[GameStatusEnum.Running]: "Stop",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "Resume"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
||||||
@ -129,8 +149,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
|
|||||||
[GameStatusEnum.Installed]: PlayIcon,
|
[GameStatusEnum.Installed]: PlayIcon,
|
||||||
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
[GameStatusEnum.Updating]: ArrowDownTrayIcon,
|
||||||
[GameStatusEnum.Uninstalling]: TrashIcon,
|
[GameStatusEnum.Uninstalling]: TrashIcon,
|
||||||
[GameStatusEnum.Running]: StopIcon,
|
[GameStatusEnum.Running]: PlayIcon,
|
||||||
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
||||||
@ -142,6 +161,5 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
|
|||||||
[GameStatusEnum.Updating]: () => emit("queue"),
|
[GameStatusEnum.Updating]: () => emit("queue"),
|
||||||
[GameStatusEnum.Uninstalling]: () => {},
|
[GameStatusEnum.Uninstalling]: () => {},
|
||||||
[GameStatusEnum.Running]: () => emit("kill"),
|
[GameStatusEnum.Running]: () => emit("kill"),
|
||||||
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 inline-flex gap-x-2">
|
|
||||||
<div
|
<div
|
||||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
class="relative mb-3 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="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
>
|
>
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||||
class="h-5 w-5 text-zinc-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -19,13 +15,6 @@
|
|||||||
placeholder="Search library..."
|
placeholder="Search library..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
@click="() => calculateGames(true)"
|
|
||||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
|
||||||
>
|
|
||||||
<ArrowPathIcon class="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
@ -71,7 +60,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
import { MagnifyingGlassIcon } 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 Game, type GameStatus } from "~/types";
|
||||||
import { TransitionGroup } from "vue";
|
import { TransitionGroup } from "vue";
|
||||||
@ -87,7 +76,6 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
|||||||
[GameStatusEnum.Updating]: "text-blue-500",
|
[GameStatusEnum.Updating]: "text-blue-500",
|
||||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-600",
|
|
||||||
};
|
};
|
||||||
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
const gameStatusText: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Remote]: "Not installed",
|
[GameStatusEnum.Remote]: "Not installed",
|
||||||
@ -98,7 +86,6 @@ const gameStatusText: { [key in GameStatusEnum]: string } = {
|
|||||||
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
[GameStatusEnum.Uninstalling]: "Uninstalling...",
|
||||||
[GameStatusEnum.SetupRequired]: "Setup required",
|
[GameStatusEnum.SetupRequired]: "Setup required",
|
||||||
[GameStatusEnum.Running]: "Running",
|
[GameStatusEnum.Running]: "Running",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "Partially installed",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -112,20 +99,16 @@ const icons: { [key: string]: string } = {};
|
|||||||
|
|
||||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||||
|
|
||||||
async function calculateGames(clearAll = false) {
|
async function calculateGames() {
|
||||||
if (clearAll) rawGames.value = [];
|
rawGames.value = await invoke("fetch_library");
|
||||||
// If we update immediately, the navigation gets re-rendered before we
|
for (const game of rawGames.value) {
|
||||||
// add all the necessary state, and it freaks tf out
|
|
||||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
|
||||||
for (const game of newGames) {
|
|
||||||
if (games[game.id]) continue;
|
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 rawGames.value) {
|
||||||
if (icons[game.id]) continue;
|
if (icons[game.id]) continue;
|
||||||
icons[game.id] = await useObject(game.mIconObjectId);
|
icons[game.id] = await useObject(game.mIconObjectId);
|
||||||
}
|
}
|
||||||
rawGames.value = newGames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await calculateGames();
|
await calculateGames();
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type SerializedGameStatus = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
|
||||||
|
console.log(status);
|
||||||
if (status[0]) {
|
if (status[0]) {
|
||||||
return {
|
return {
|
||||||
type: status[0].type,
|
type: status[0].type,
|
||||||
@ -47,6 +48,7 @@ export const useGame = async (gameId: string) => {
|
|||||||
status: SerializedGameStatus;
|
status: SerializedGameStatus;
|
||||||
version?: GameVersion;
|
version?: GameVersion;
|
||||||
} = event.payload as any;
|
} = event.payload as any;
|
||||||
|
console.log(payload.status);
|
||||||
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
gameStatusRegistry[gameId].value = parseStatus(payload.status);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { data } from "autoprefixer";
|
import { data } from "autoprefixer";
|
||||||
import { AppStatus, type AppState } from "~/types";
|
import { AppStatus, type AppState } from "~/types";
|
||||||
|
|
||||||
export function setupHooks() {
|
export function setupHooks() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state = useAppState();
|
|
||||||
|
|
||||||
listen("auth/processing", (event) => {
|
listen("auth/processing", (event) => {
|
||||||
router.push("/auth/processing");
|
router.push("/auth/processing");
|
||||||
@ -17,9 +15,8 @@ export function setupHooks() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("auth/finished", async (event) => {
|
listen("auth/finished", (event) => {
|
||||||
router.push("/library");
|
router.push("/store");
|
||||||
state.value = JSON.parse(await invoke("fetch_state"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("download_error", (event) => {
|
listen("download_error", (event) => {
|
||||||
@ -30,31 +27,12 @@ export function setupHooks() {
|
|||||||
description: `Drop encountered an error while downloading your game: "${(
|
description: `Drop encountered an error while downloading your game: "${(
|
||||||
event.payload as unknown as string
|
event.payload as unknown as string
|
||||||
).toString()}"`,
|
).toString()}"`,
|
||||||
buttonText: "Close",
|
buttonText: "Close"
|
||||||
},
|
},
|
||||||
(e, c) => c()
|
(e, c) => c()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is for errors that (we think) aren't our fault
|
|
||||||
listen("launch_external_error", (event) => {
|
|
||||||
createModal(
|
|
||||||
ModalType.Confirmation,
|
|
||||||
{
|
|
||||||
title: "Did something go wrong?",
|
|
||||||
description:
|
|
||||||
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
|
|
||||||
buttonText: "Open",
|
|
||||||
},
|
|
||||||
async (e, c) => {
|
|
||||||
if (e == "confirm") {
|
|
||||||
await invoke("open_process_logs", { gameId: event.payload });
|
|
||||||
}
|
|
||||||
c();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
document.addEventListener("contextmenu", (event) => {
|
document.addEventListener("contextmenu", (event) => {
|
||||||
@ -82,6 +60,6 @@ export function initialNavigation(state: Ref<AppState>) {
|
|||||||
router.push("/error/serverunavailable");
|
router.push("/error/serverunavailable");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
router.push("/library");
|
router.push("/store");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "drop-app",
|
"name": "drop-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.0-rc-8",
|
"version": "0.3.0-rc-3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@ -18,7 +18,6 @@
|
|||||||
"@tauri-apps/api": ">=2.0.0",
|
"@tauri-apps/api": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
|
||||||
"@tauri-apps/plugin-os": "~2",
|
"@tauri-apps/plugin-os": "~2",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||||
"koa": "^2.16.1",
|
"koa": "^2.16.1",
|
||||||
|
|||||||
@ -32,7 +32,6 @@
|
|||||||
@uninstall="() => uninstall()"
|
@uninstall="() => uninstall()"
|
||||||
@kill="() => kill()"
|
@kill="() => kill()"
|
||||||
@options="() => (configureModalOpen = true)"
|
@options="() => (configureModalOpen = true)"
|
||||||
@resume="() => resumeDownload()"
|
|
||||||
:status="status"
|
:status="status"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
@ -78,7 +77,6 @@
|
|||||||
v-for="(url, index) in mediaUrls"
|
v-for="(url, index) in mediaUrls"
|
||||||
:key="url"
|
:key="url"
|
||||||
:src="url"
|
:src="url"
|
||||||
loading="lazy"
|
|
||||||
class="absolute inset-0 w-full h-full object-cover"
|
class="absolute inset-0 w-full h-full object-cover"
|
||||||
v-show="index === currentImageIndex"
|
v-show="index === currentImageIndex"
|
||||||
/>
|
/>
|
||||||
@ -497,7 +495,6 @@ const currentImageIndex = ref(0);
|
|||||||
|
|
||||||
const configureModalOpen = ref(false);
|
const configureModalOpen = ref(false);
|
||||||
|
|
||||||
|
|
||||||
async function installFlow() {
|
async function installFlow() {
|
||||||
installFlowOpen.value = true;
|
installFlowOpen.value = true;
|
||||||
versionOptions.value = undefined;
|
versionOptions.value = undefined;
|
||||||
@ -535,15 +532,6 @@ async function install() {
|
|||||||
installLoading.value = false;
|
installLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeDownload() {
|
|
||||||
try {
|
|
||||||
await invoke("resume_download", { gameId: game.value.id })
|
|
||||||
}
|
|
||||||
catch(e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function launch() {
|
async function launch() {
|
||||||
try {
|
try {
|
||||||
await invoke("launch_game", { id: game.value.id });
|
await invoke("launch_game", { id: game.value.id });
|
||||||
|
|||||||
@ -91,12 +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, Game, GameStatus } from "~/types";
|
||||||
|
|
||||||
// const actionNames = {
|
|
||||||
// [GameStatusEnum.Downloading]: "downloading",
|
|
||||||
// [GameStatusEnum.Verifying]: "verifying",
|
|
||||||
// }
|
|
||||||
|
|
||||||
const windowWidth = ref(window.innerWidth);
|
const windowWidth = ref(window.innerWidth);
|
||||||
window.addEventListener("resize", (event) => {
|
window.addEventListener("resize", (event) => {
|
||||||
|
|||||||
@ -106,6 +106,8 @@ const systemData = await invoke<{
|
|||||||
dataDir: string;
|
dataDir: string;
|
||||||
}>("fetch_system_data");
|
}>("fetch_system_data");
|
||||||
|
|
||||||
|
console.log(systemData);
|
||||||
|
|
||||||
clientId.value = systemData.clientId;
|
clientId.value = systemData.clientId;
|
||||||
baseUrl.value = systemData.baseUrl;
|
baseUrl.value = systemData.baseUrl;
|
||||||
dataDir.value = systemData.dataDir;
|
dataDir.value = systemData.dataDir;
|
||||||
|
|||||||
@ -1,37 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grow w-full h-full flex items-center justify-center">
|
<iframe src="server://drop.local/store" class="w-full h-full" />
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<BuildingStorefrontIcon
|
|
||||||
class="h-12 w-12 text-blue-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="mt-3 text-center sm:mt-5">
|
|
||||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Store not supported in client
|
|
||||||
</h1>
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-zinc-400 max-w-lg">
|
|
||||||
Currently, Drop requires you to view the store in your browser.
|
|
||||||
Please click the button below to open it in your default browser.
|
|
||||||
</p>
|
|
||||||
<NuxtLink
|
|
||||||
:href="storeUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="mt-6 transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
|
||||||
>
|
|
||||||
Open Store <ArrowTopRightOnSquareIcon class="size-4" />
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import {
|
|
||||||
ArrowTopRightOnSquareIcon,
|
|
||||||
BuildingStorefrontIcon,
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
|
|
||||||
const storeUrl = await invoke<string>("gen_drop_url", { path: "/store" });
|
|
||||||
</script>
|
|
||||||
|
|||||||
1387
src-tauri/Cargo.lock
generated
1387
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.0-rc-8"
|
version = "0.3.0-rc-3"
|
||||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||||
authors = ["Drop OSS"]
|
authors = ["Drop OSS"]
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@ -25,6 +25,8 @@ tauri-build = { version = "2.0.0", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri-plugin-shell = "2.2.1"
|
tauri-plugin-shell = "2.2.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde-binary = "0.5.0"
|
||||||
|
rayon = "1.10.0"
|
||||||
webbrowser = "1.0.2"
|
webbrowser = "1.0.2"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
@ -65,13 +67,6 @@ 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.1", features = ["rmp_serde_1_3"] }
|
||||||
tauri-plugin-opener = "2.4.0"
|
|
||||||
bitcode = "0.6.6"
|
|
||||||
async-trait = "0.1.88"
|
|
||||||
futures = "0.3.31"
|
|
||||||
tokio-util = { version = "0.7.15", features = ["io"] }
|
|
||||||
async-scoped = { version = "0.9.0", features = ["use-tokio"] }
|
|
||||||
async-once-cell = "0.5.4"
|
|
||||||
# tailscale = { path = "./tailscale" }
|
# tailscale = { path = "./tailscale" }
|
||||||
|
|
||||||
[dependencies.dynfmt]
|
[dependencies.dynfmt]
|
||||||
@ -80,7 +75,7 @@ features = ["curly"]
|
|||||||
|
|
||||||
[dependencies.tauri]
|
[dependencies.tauri]
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
features = ["protocol-asset", "tray-icon"]
|
features = ["tray-icon"]
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.40.0"
|
version = "1.40.0"
|
||||||
@ -98,14 +93,14 @@ features = ["fs"]
|
|||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||||
|
|
||||||
[dependencies.dropbreak]
|
[dependencies.rustbreak]
|
||||||
git = "https://github.com/Drop-OSS/dropbreak.git"
|
version = "2"
|
||||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12"
|
version = "0.12"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["json", "http2", "rustls-tls-webpki-roots", "stream"]
|
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"os:default",
|
"os:default"
|
||||||
"opener:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
|
||||||
pub async fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
let manager = app.autolaunch();
|
let manager = app.autolaunch();
|
||||||
if enabled {
|
if enabled {
|
||||||
manager.enable().map_err(|e| e.to_string())?;
|
manager.enable().map_err(|e| e.to_string())?;
|
||||||
@ -14,18 +14,17 @@ pub async fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the state in DB
|
// Store the state in DB
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle.settings.autostart = enabled;
|
db_handle.settings.autostart = enabled;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
save_db();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_autostart_enabled_logic(
|
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||||
app: AppHandle,
|
|
||||||
) -> Result<bool, tauri_plugin_autostart::Error> {
|
|
||||||
// First check DB state
|
// First check DB state
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
let db_state = db_handle.settings.autostart;
|
let db_state = db_handle.settings.autostart;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
@ -46,8 +45,8 @@ pub async fn get_autostart_enabled_logic(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New function to sync state on startup
|
// New function to sync state on startup
|
||||||
pub async fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
let should_be_enabled = db_handle.settings.autostart;
|
let should_be_enabled = db_handle.settings.autostart;
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
@ -67,11 +66,11 @@ pub async fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||||
toggle_autostart_logic(app, enabled).await
|
toggle_autostart_logic(app, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||||
get_autostart_enabled_logic(app).await
|
get_autostart_enabled_logic(app)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,22 @@
|
|||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::DropFunctionState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn quit<>(app: tauri::AppHandle, state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
cleanup_and_exit(&app, &state).await;
|
cleanup_and_exit(&app, &state);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cleanup_and_exit(
|
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
app: &AppHandle,
|
|
||||||
state: &tauri::State<'_, DropFunctionState<'_>>,
|
|
||||||
) {
|
|
||||||
debug!("cleaning up and exiting application");
|
debug!("cleaning up and exiting application");
|
||||||
let download_manager = state.lock().await.download_manager.clone();
|
let download_manager = state.lock().unwrap().download_manager.clone();
|
||||||
match download_manager.ensure_terminated().await {
|
match download_manager.ensure_terminated() {
|
||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
Ok(_) => debug!("download manager terminated correctly"),
|
Ok(_) => debug!("download manager terminated correctly"),
|
||||||
Err(_) => error!("download manager failed to terminate correctly"),
|
Err(_) => error!("download manager failed to terminate correctly"),
|
||||||
},
|
},
|
||||||
Err(e) => panic!("{e:?}"),
|
Err(e) => panic!("{:?}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use crate::DropFunctionState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_state(
|
pub fn fetch_state(
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let guard = state.lock().await;
|
let guard = state.lock().unwrap();
|
||||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
Ok(cloned_state)
|
Ok(cloned_state)
|
||||||
|
|||||||
@ -6,12 +6,10 @@ use std::{
|
|||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
|
||||||
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
db::{DATA_ROOT_DIR, borrow_db_checked},
|
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
|
||||||
debug::SystemData,
|
debug::SystemData,
|
||||||
models::data::Settings,
|
models::data::Settings,
|
||||||
};
|
};
|
||||||
@ -19,19 +17,21 @@ use super::{
|
|||||||
// Will, in future, return disk/remaining size
|
// Will, in future, return disk/remaining size
|
||||||
// Just returns the directories that have been set up
|
// Just returns the directories that have been set up
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||||
let lock = borrow_db_checked().await;
|
let lock = borrow_db_checked();
|
||||||
lock.applications.install_dirs.clone()
|
lock.applications.install_dirs.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_download_dir(index: usize) {
|
pub fn delete_download_dir(index: usize) {
|
||||||
let mut lock = borrow_db_mut_checked().await;
|
let mut lock = borrow_db_mut_checked();
|
||||||
lock.applications.install_dirs.remove(index);
|
lock.applications.install_dirs.remove(index);
|
||||||
|
drop(lock);
|
||||||
|
save_db();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
||||||
// Check the new directory is all good
|
// Check the new directory is all good
|
||||||
let new_dir_path = Path::new(&new_dir);
|
let new_dir_path = Path::new(&new_dir);
|
||||||
if new_dir_path.exists() {
|
if new_dir_path.exists() {
|
||||||
@ -48,7 +48,7 @@ pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add it to the dictionary
|
// Add it to the dictionary
|
||||||
let mut lock = borrow_db_mut_checked().await;
|
let mut lock = borrow_db_mut_checked();
|
||||||
if lock.applications.install_dirs.contains(&new_dir) {
|
if lock.applications.install_dirs.contains(&new_dir) {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::AlreadyExists,
|
ErrorKind::AlreadyExists,
|
||||||
@ -58,31 +58,34 @@ pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerErr
|
|||||||
}
|
}
|
||||||
lock.applications.install_dirs.push(new_dir);
|
lock.applications.install_dirs.push(new_dir);
|
||||||
drop(lock);
|
drop(lock);
|
||||||
|
save_db();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_settings(new_settings: Value) {
|
pub fn update_settings(new_settings: Value) {
|
||||||
let mut db_lock = borrow_db_mut_checked().await;
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
||||||
for (key, value) in new_settings.as_object().unwrap() {
|
for (key, value) in new_settings.as_object().unwrap() {
|
||||||
current_settings[key] = value.clone();
|
current_settings[key] = value.clone();
|
||||||
}
|
}
|
||||||
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
|
||||||
db_lock.settings = new_settings;
|
db_lock.settings = new_settings;
|
||||||
|
drop(db_lock);
|
||||||
|
save_db();
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_settings() -> Settings {
|
pub fn fetch_settings() -> Settings {
|
||||||
borrow_db_checked().await.settings.clone()
|
borrow_db_checked().settings.clone()
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_system_data() -> SystemData {
|
pub fn fetch_system_data() -> SystemData {
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
SystemData::new(
|
SystemData::new(
|
||||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||||
db_handle.base_url.clone(),
|
db_handle.base_url.clone(),
|
||||||
DATA_ROOT_DIR.to_string_lossy().to_string(),
|
DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
|
||||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,23 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::{self, create_dir_all},
|
fs::{self, create_dir_all},
|
||||||
mem::ManuallyDrop,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, LazyLock},
|
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_once_cell::OnceCell;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use dropbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
use log::{debug, error, info, warn};
|
||||||
use log::{debug, info, warn};
|
|
||||||
use native_model::{Decode, Encode};
|
use native_model::{Decode, Encode};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||||
use tokio::{
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
spawn,
|
|
||||||
sync::{RwLockReadGuard, RwLockWriteGuard},
|
|
||||||
};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::DB;
|
use crate::DB;
|
||||||
|
|
||||||
use super::models::data::Database;
|
use super::models::data::Database;
|
||||||
|
|
||||||
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
|
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
||||||
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop")));
|
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
|
||||||
|
|
||||||
|
|
||||||
// Custom JSON serializer to support everything we need
|
// Custom JSON serializer to support everything we need
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@ -32,57 +26,39 @@ pub struct DropDatabaseSerializer;
|
|||||||
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||||
for DropDatabaseSerializer
|
for DropDatabaseSerializer
|
||||||
{
|
{
|
||||||
fn serialize(&self, val: &T) -> dropbreak::error::DeSerResult<Vec<u8>> {
|
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||||
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
native_model::rmp_serde_1_3::RmpSerde::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
||||||
.map_err(|e| DeSerError::Internal(e.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> dropbreak::error::DeSerResult<T> {
|
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
s.read_to_end(&mut buf)
|
s.read_to_end(&mut buf)
|
||||||
.map_err(|e| dropbreak::error::DeSerError::Other(e.into()))?;
|
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||||
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
let val =
|
||||||
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||||
Ok(val)
|
Ok(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DatabaseInterface =
|
pub type DatabaseInterface =
|
||||||
dropbreak::Database<Database, dropbreak::backend::PathBackend, DropDatabaseSerializer>;
|
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
|
||||||
|
|
||||||
pub struct OnceCellDatabase(OnceCell<DatabaseInterface>);
|
|
||||||
impl OnceCellDatabase {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self(OnceCell::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init(&self, init: impl Future<Output = DatabaseInterface>) {
|
|
||||||
self.0.get_or_init(init).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Deref for OnceCellDatabase {
|
|
||||||
type Target = DatabaseInterface;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0.get().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DatabaseImpls {
|
pub trait DatabaseImpls {
|
||||||
async fn set_up_database() -> DatabaseInterface;
|
fn set_up_database() -> DatabaseInterface;
|
||||||
async fn database_is_set_up(&self) -> bool;
|
fn database_is_set_up(&self) -> bool;
|
||||||
async fn fetch_base_url(&self) -> Url;
|
fn fetch_base_url(&self) -> Url;
|
||||||
}
|
}
|
||||||
impl DatabaseImpls for DatabaseInterface {
|
impl DatabaseImpls for DatabaseInterface {
|
||||||
async fn set_up_database() -> DatabaseInterface {
|
fn set_up_database() -> DatabaseInterface {
|
||||||
let db_path = DATA_ROOT_DIR.join("drop.db");
|
let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
|
||||||
let games_base_dir = DATA_ROOT_DIR.join("games");
|
let db_path = data_root_dir.join("drop.db");
|
||||||
let logs_root_dir = DATA_ROOT_DIR.join("logs");
|
let games_base_dir = data_root_dir.join("games");
|
||||||
let cache_dir = DATA_ROOT_DIR.join("cache");
|
let logs_root_dir = data_root_dir.join("logs");
|
||||||
let pfx_dir = DATA_ROOT_DIR.join("pfx");
|
let cache_dir = data_root_dir.join("cache");
|
||||||
|
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.clone()).unwrap();
|
||||||
create_dir_all(&games_base_dir).unwrap();
|
create_dir_all(&games_base_dir).unwrap();
|
||||||
create_dir_all(&logs_root_dir).unwrap();
|
create_dir_all(&logs_root_dir).unwrap();
|
||||||
create_dir_all(&cache_dir).unwrap();
|
create_dir_all(&cache_dir).unwrap();
|
||||||
@ -91,9 +67,9 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
let exists = fs::exists(db_path.clone()).unwrap();
|
let exists = fs::exists(db_path.clone()).unwrap();
|
||||||
|
|
||||||
match exists {
|
match exists {
|
||||||
true => match PathDatabase::load_from_path(db_path.clone()).await {
|
true => match PathDatabase::load_from_path(db_path.clone()) {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir).await,
|
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||||
},
|
},
|
||||||
false => {
|
false => {
|
||||||
let default = Database::new(games_base_dir, None, cache_dir);
|
let default = Database::new(games_base_dir, None, cache_dir);
|
||||||
@ -102,36 +78,39 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
db_path.as_os_str().to_str().unwrap()
|
db_path.as_os_str().to_str().unwrap()
|
||||||
);
|
);
|
||||||
PathDatabase::create_at_path(db_path, default)
|
PathDatabase::create_at_path(db_path, default)
|
||||||
.await
|
|
||||||
.expect("Database could not be created")
|
.expect("Database could not be created")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn database_is_set_up(&self) -> bool {
|
fn database_is_set_up(&self) -> bool {
|
||||||
!self.borrow_data().await.base_url.is_empty()
|
!self.borrow_data().unwrap().base_url.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_base_url(&self) -> Url {
|
fn fetch_base_url(&self) -> Url {
|
||||||
let handle = self.borrow_data().await;
|
let handle = self.borrow_data().unwrap();
|
||||||
Url::parse(&handle.base_url).unwrap()
|
Url::parse(&handle.base_url).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_invalid_database(
|
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||||
|
fn handle_invalid_database(
|
||||||
_e: RustbreakError,
|
_e: RustbreakError,
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
games_base_dir: PathBuf,
|
games_base_dir: PathBuf,
|
||||||
cache_dir: PathBuf,
|
cache_dir: PathBuf,
|
||||||
) -> dropbreak::Database<Database, dropbreak::backend::PathBackend, DropDatabaseSerializer> {
|
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||||
warn!("{_e}");
|
warn!("{}", _e);
|
||||||
let new_path = {
|
let new_path = {
|
||||||
let time = Utc::now().timestamp();
|
let time = Utc::now().timestamp();
|
||||||
let mut base = db_path.clone();
|
let mut base = db_path.clone();
|
||||||
base.set_file_name(format!("drop.db.backup-{time}"));
|
base.set_file_name(format!("drop.db.backup-{}", time));
|
||||||
base
|
base
|
||||||
};
|
};
|
||||||
info!("old database stored at: {}", new_path.to_string_lossy());
|
info!(
|
||||||
|
"old database stored at: {}",
|
||||||
|
new_path.to_string_lossy().to_string()
|
||||||
|
);
|
||||||
fs::rename(&db_path, &new_path).unwrap();
|
fs::rename(&db_path, &new_path).unwrap();
|
||||||
|
|
||||||
let db = Database::new(
|
let db = Database::new(
|
||||||
@ -140,53 +119,35 @@ async fn handle_invalid_database(
|
|||||||
cache_dir,
|
cache_dir,
|
||||||
);
|
);
|
||||||
|
|
||||||
PathDatabase::create_at_path(db_path, db)
|
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||||
.await
|
|
||||||
.expect("Database could not be created")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// To automatically save the database upon drop
|
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
|
||||||
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
match DB.borrow_data() {
|
||||||
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
Ok(data) => data,
|
||||||
impl<'a> Deref for DBWrite<'a> {
|
Err(e) => {
|
||||||
type Target = RwLockWriteGuard<'a, Database>;
|
error!("database borrow failed with error {}", e);
|
||||||
|
panic!("database borrow failed with error {}", e);
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a> Deref for DBRead<'a> {
|
|
||||||
type Target = RwLockReadGuard<'a, Database>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> DerefMut for DBWrite<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Drop for DBWrite<'a> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe {
|
|
||||||
ManuallyDrop::drop(&mut self.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn(async {
|
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
|
||||||
match DB.save().await {
|
match DB.borrow_data_mut() {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
error!("database borrow mut failed with error {}", e);
|
||||||
|
panic!("database borrow mut failed with error {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_db() {
|
||||||
|
match DB.save() {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
panic!("database failed to save with error {e}")
|
error!("database failed to save with error {}", e);
|
||||||
|
panic!("database failed to save with error {}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub async fn borrow_db_checked<'a>() -> DBRead<'a> {
|
|
||||||
DBRead(DB.borrow_data().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
|
||||||
DBWrite(ManuallyDrop::new(DB.borrow_data_mut().await))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,19 @@
|
|||||||
use crate::database::models::data::Database;
|
|
||||||
|
|
||||||
pub mod data {
|
pub mod data {
|
||||||
use std::path::PathBuf;
|
use native_model::{native_model, Model};
|
||||||
|
|
||||||
use native_model::native_model;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub type GameVersion = v1::GameVersion;
|
pub type GameVersion = v1::GameVersion;
|
||||||
pub type Database = v3::Database;
|
pub type Database = v2::Database;
|
||||||
pub type Settings = v1::Settings;
|
pub type Settings = v1::Settings;
|
||||||
pub type DatabaseAuth = v1::DatabaseAuth;
|
pub type DatabaseAuth = v1::DatabaseAuth;
|
||||||
|
|
||||||
pub type GameDownloadStatus = v2::GameDownloadStatus;
|
pub type GameDownloadStatus = v1::GameDownloadStatus;
|
||||||
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
||||||
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 = v1::DatabaseApplications;
|
||||||
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||||
|
|
||||||
use std::{collections::HashMap, process::Command};
|
|
||||||
|
|
||||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
|
||||||
|
|
||||||
pub mod v1 {
|
pub mod v1 {
|
||||||
use crate::process::process_manager::Platform;
|
use crate::process::process_manager::Platform;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
@ -110,7 +102,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)]
|
||||||
pub enum ApplicationTransientStatus {
|
pub enum ApplicationTransientStatus {
|
||||||
Downloading { version_name: String },
|
Downloading { version_name: String },
|
||||||
Uninstalling {},
|
Uninstalling {},
|
||||||
@ -134,7 +126,7 @@ pub mod data {
|
|||||||
pub enum DownloadType {
|
pub enum DownloadType {
|
||||||
Game,
|
Game,
|
||||||
Tool,
|
Tool,
|
||||||
Dlc,
|
DLC,
|
||||||
Mod,
|
Mod,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,122 +162,14 @@ pub mod data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod v2 {
|
pub mod v2 {
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf, process::Command};
|
||||||
|
|
||||||
use serde_with::serde_as;
|
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[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)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct Database {
|
|
||||||
#[serde(default)]
|
|
||||||
pub settings: Settings,
|
|
||||||
pub auth: Option<DatabaseAuth>,
|
|
||||||
pub base_url: String,
|
|
||||||
pub applications: v1::DatabaseApplications,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub prev_database: Option<PathBuf>,
|
|
||||||
pub cache_dir: PathBuf,
|
|
||||||
pub compat_info: Option<DatabaseCompatInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
|
||||||
|
|
||||||
pub struct DatabaseCompatInfo {
|
|
||||||
pub umu_installed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<v1::Database> for Database {
|
|
||||||
fn from(value: v1::Database) -> Self {
|
|
||||||
Self {
|
|
||||||
settings: value.settings,
|
|
||||||
auth: value.auth,
|
|
||||||
base_url: value.base_url,
|
|
||||||
applications: value.applications,
|
|
||||||
prev_database: value.prev_database,
|
|
||||||
cache_dir: value.cache_dir,
|
|
||||||
compat_info: crate::database::models::Database::create_new_compat_info(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Strings are version names for a particular game
|
|
||||||
#[derive(Serialize, Clone, Deserialize, Debug)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
pub enum GameDownloadStatus {
|
|
||||||
Remote {},
|
|
||||||
SetupRequired {
|
|
||||||
version_name: String,
|
|
||||||
install_dir: String,
|
|
||||||
},
|
|
||||||
Installed {
|
|
||||||
version_name: String,
|
|
||||||
install_dir: String,
|
|
||||||
},
|
|
||||||
PartiallyInstalled {
|
|
||||||
version_name: String,
|
|
||||||
install_dir: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
|
|
||||||
fn from(value: v1::GameDownloadStatus) -> Self {
|
|
||||||
match value {
|
|
||||||
v1::GameDownloadStatus::Remote {} => Self::Remote {},
|
|
||||||
v1::GameDownloadStatus::SetupRequired {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
} => Self::SetupRequired {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
},
|
|
||||||
v1::GameDownloadStatus::Installed {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
} => Self::Installed {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Serialize, Clone, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
pub struct DatabaseApplications {
|
|
||||||
pub install_dirs: Vec<PathBuf>,
|
|
||||||
// Guaranteed to exist if the game also exists in the app state map
|
|
||||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
|
||||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
|
||||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
|
||||||
}
|
|
||||||
impl From<v1::DatabaseApplications> for DatabaseApplications {
|
|
||||||
fn from(value: v1::DatabaseApplications) -> Self {
|
|
||||||
Self {
|
|
||||||
game_statuses: value
|
|
||||||
.game_statuses
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| (x.0, x.1.into()))
|
|
||||||
.collect::<HashMap<String, GameDownloadStatus>>(),
|
|
||||||
install_dirs: value.install_dirs,
|
|
||||||
game_versions: value.game_versions,
|
|
||||||
installed_game_version: value.installed_game_version,
|
|
||||||
transient_statuses: value.transient_statuses,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mod v3 {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
@ -297,20 +181,14 @@ pub mod data {
|
|||||||
pub cache_dir: PathBuf,
|
pub cache_dir: PathBuf,
|
||||||
pub compat_info: Option<DatabaseCompatInfo>,
|
pub compat_info: Option<DatabaseCompatInfo>,
|
||||||
}
|
}
|
||||||
impl From<v2::Database> for Database {
|
|
||||||
fn from(value: v2::Database) -> Self {
|
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
Self {
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
settings: value.settings,
|
|
||||||
auth: value.auth,
|
pub struct DatabaseCompatInfo {
|
||||||
base_url: value.base_url,
|
umu_installed: bool,
|
||||||
applications: value.applications.into(),
|
|
||||||
prev_database: value.prev_database,
|
|
||||||
cache_dir: value.cache_dir,
|
|
||||||
compat_info: Database::create_new_compat_info(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
|
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@ -344,4 +222,19 @@ pub mod data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<v1::Database> for Database {
|
||||||
|
fn from(value: v1::Database) -> Self {
|
||||||
|
Self {
|
||||||
|
settings: value.settings,
|
||||||
|
auth: value.auth,
|
||||||
|
base_url: value.base_url,
|
||||||
|
applications: value.applications,
|
||||||
|
prev_database: value.prev_database,
|
||||||
|
cache_dir: value.cache_dir,
|
||||||
|
compat_info: Database::create_new_compat_info(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,31 @@
|
|||||||
use crate::{database::models::data::DownloadableMetadata, DropFunctionState};
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn pause_downloads(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
state.lock().await.download_manager.pause_downloads();
|
state.lock().unwrap().download_manager.pause_downloads()
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn resume_downloads(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
state.lock().await.download_manager.resume_downloads();
|
state.lock().unwrap().download_manager.resume_downloads()
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn move_download_in_queue(
|
pub fn move_download_in_queue(
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
old_index: usize,
|
old_index: usize,
|
||||||
new_index: usize,
|
new_index: usize,
|
||||||
) -> Result<(), ()> {
|
) {
|
||||||
state
|
state
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.unwrap()
|
||||||
.download_manager
|
.download_manager
|
||||||
.rearrange(old_index, new_index);
|
.rearrange(old_index, new_index)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn cancel_game(state: tauri::State<'_, DropFunctionState<'_>>, meta: DownloadableMetadata) -> Result<(), ()> {
|
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||||
state.lock().await.download_manager.cancel(meta);
|
state.lock().unwrap().download_manager.cancel(meta)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
sync::{
|
sync::{
|
||||||
mpsc::{SendError, Sender},
|
mpsc::{SendError, Sender},
|
||||||
Mutex, MutexGuard,
|
Mutex, MutexGuard,
|
||||||
},
|
},
|
||||||
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::async_runtime::JoinHandle;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
database::models::data::DownloadableMetadata,
|
||||||
@ -17,8 +18,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
download_manager_builder::{CurrentProgressObject, DownloadAgent}, util::queue::Queue,
|
||||||
util::queue::Queue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum DownloadManagerSignal {
|
pub enum DownloadManagerSignal {
|
||||||
@ -37,11 +37,16 @@ pub enum DownloadManagerSignal {
|
|||||||
Finish,
|
Finish,
|
||||||
/// Stops, removes, and tells a download to cleanup
|
/// Stops, removes, and tells a download to cleanup
|
||||||
Cancel(DownloadableMetadata),
|
Cancel(DownloadableMetadata),
|
||||||
|
/// Removes a given application
|
||||||
|
Remove(DownloadableMetadata),
|
||||||
/// Any error which occurs in the agent
|
/// Any error which occurs in the agent
|
||||||
Error(ApplicationDownloadError),
|
Error(ApplicationDownloadError),
|
||||||
/// Pushes UI update
|
/// Pushes UI update
|
||||||
UpdateUIQueue,
|
UpdateUIQueue,
|
||||||
UpdateUIStats(usize, usize), //kb/s and seconds
|
UpdateUIStats(usize, usize), //kb/s and seconds
|
||||||
|
/// Uninstall download
|
||||||
|
/// Takes download ID
|
||||||
|
Uninstall(DownloadableMetadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -49,7 +54,8 @@ pub enum DownloadManagerStatus {
|
|||||||
Downloading,
|
Downloading,
|
||||||
Paused,
|
Paused,
|
||||||
Empty,
|
Empty,
|
||||||
Error,
|
Error(ApplicationDownloadError),
|
||||||
|
Finished,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for DownloadManagerStatus {
|
impl Serialize for DownloadManagerStatus {
|
||||||
@ -57,7 +63,7 @@ impl Serialize for DownloadManagerStatus {
|
|||||||
where
|
where
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_str(&format!["{self:?}"])
|
serializer.serialize_str(&format!["{:?}", self])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +71,6 @@ impl Serialize for DownloadManagerStatus {
|
|||||||
pub enum DownloadStatus {
|
pub enum DownloadStatus {
|
||||||
Queued,
|
Queued,
|
||||||
Downloading,
|
Downloading,
|
||||||
Validating,
|
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,8 +122,8 @@ impl DownloadManager {
|
|||||||
pub fn read_queue(&self) -> VecDeque<DownloadableMetadata> {
|
pub fn read_queue(&self) -> VecDeque<DownloadableMetadata> {
|
||||||
self.download_queue.read()
|
self.download_queue.read()
|
||||||
}
|
}
|
||||||
pub async fn get_current_download_progress(&self) -> Option<f64> {
|
pub fn get_current_download_progress(&self) -> Option<f64> {
|
||||||
let progress_object = (*self.progress.lock().await).clone()?;
|
let progress_object = (*self.progress.lock().unwrap()).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) {
|
||||||
@ -147,7 +152,10 @@ impl DownloadManager {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("moving download at index {current_index} to index {new_index}");
|
debug!(
|
||||||
|
"moving download at index {} to index {}",
|
||||||
|
current_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).unwrap();
|
||||||
@ -170,12 +178,17 @@ impl DownloadManager {
|
|||||||
pub fn resume_downloads(&self) {
|
pub fn resume_downloads(&self) {
|
||||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
||||||
}
|
}
|
||||||
pub async fn ensure_terminated(&self) -> Result<Result<(), ()>, tauri::Error> {
|
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
|
||||||
self.command_sender
|
self.command_sender
|
||||||
.send(DownloadManagerSignal::Finish)
|
.send(DownloadManagerSignal::Finish)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let terminator = self.terminator.lock().unwrap().take();
|
let terminator = self.terminator.lock().unwrap().take();
|
||||||
terminator.unwrap().await
|
terminator.unwrap().join()
|
||||||
|
}
|
||||||
|
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
|
||||||
|
self.command_sender
|
||||||
|
.send(DownloadManagerSignal::Uninstall(meta))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
||||||
self.command_sender.clone()
|
self.command_sender.clone()
|
||||||
@ -1,15 +1,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
mpsc::{channel, Receiver, Sender},
|
||||||
mpsc::{Receiver, Sender, channel},
|
Arc, Mutex,
|
||||||
},
|
},
|
||||||
|
thread::{spawn, JoinHandle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::futures::future::join_all;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use tokio::{runtime::Runtime, sync::Mutex};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::models::data::DownloadableMetadata,
|
database::models::data::DownloadableMetadata,
|
||||||
@ -18,13 +17,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_frontend::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||||
downloadable::Downloadable,
|
downloadable::Downloadable, util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue},
|
||||||
util::{
|
|
||||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
|
||||||
progress_object::ProgressObject,
|
|
||||||
queue::Queue,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||||
@ -70,15 +64,14 @@ Behold, my madness - quexeky
|
|||||||
pub struct DownloadManagerBuilder {
|
pub struct DownloadManagerBuilder {
|
||||||
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
||||||
download_queue: Queue,
|
download_queue: Queue,
|
||||||
command_receiver: Mutex<Receiver<DownloadManagerSignal>>,
|
command_receiver: Receiver<DownloadManagerSignal>,
|
||||||
sender: Sender<DownloadManagerSignal>,
|
sender: Sender<DownloadManagerSignal>,
|
||||||
progress: CurrentProgressObject,
|
progress: CurrentProgressObject,
|
||||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
runtime: Runtime,
|
|
||||||
|
|
||||||
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
||||||
current_download_thread: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||||
active_control_flag: Option<DownloadThreadControl>,
|
active_control_flag: Option<DownloadThreadControl>,
|
||||||
}
|
}
|
||||||
impl DownloadManagerBuilder {
|
impl DownloadManagerBuilder {
|
||||||
@ -91,121 +84,109 @@ impl DownloadManagerBuilder {
|
|||||||
let manager = Self {
|
let manager = Self {
|
||||||
download_agent_registry: HashMap::new(),
|
download_agent_registry: HashMap::new(),
|
||||||
download_queue: queue.clone(),
|
download_queue: queue.clone(),
|
||||||
command_receiver: Mutex::new(command_receiver),
|
command_receiver,
|
||||||
status: status.clone(),
|
status: status.clone(),
|
||||||
sender: command_sender.clone(),
|
sender: command_sender.clone(),
|
||||||
progress: active_progress.clone(),
|
progress: active_progress.clone(),
|
||||||
app_handle,
|
app_handle,
|
||||||
runtime: tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.worker_threads(1)
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
|
|
||||||
current_download_agent: None,
|
current_download_agent: None,
|
||||||
current_download_thread: Mutex::new(None),
|
current_download_thread: Mutex::new(None),
|
||||||
active_control_flag: None,
|
active_control_flag: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let terminator = tauri::async_runtime::spawn(async {
|
let terminator = spawn(|| manager.manage_queue());
|
||||||
if let Err(_err) = manager.manage_queue().await {
|
|
||||||
panic!("download manager exited with error");
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_status(&self, status: DownloadManagerStatus) {
|
fn set_status(&self, status: DownloadManagerStatus) {
|
||||||
*self.status.lock().await = status;
|
*self.status.lock().unwrap() = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_and_cleanup_front_download(
|
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
||||||
&mut self,
|
|
||||||
meta: &DownloadableMetadata,
|
|
||||||
) -> DownloadAgent {
|
|
||||||
self.download_queue.pop_front();
|
self.download_queue.pop_front();
|
||||||
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
||||||
self.cleanup_current_download().await;
|
self.cleanup_current_download();
|
||||||
download_agent
|
download_agent
|
||||||
}
|
}
|
||||||
|
|
||||||
// CAREFUL WITH THIS FUNCTION
|
// CAREFUL WITH THIS FUNCTION
|
||||||
// Make sure the download thread is terminated
|
// Make sure the download thread is terminated
|
||||||
async 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().await = None;
|
*self.progress.lock().unwrap() = None;
|
||||||
self.current_download_agent = None;
|
self.current_download_agent = None;
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
*download_thread_lock = None;
|
*download_thread_lock = None;
|
||||||
|
drop(download_thread_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_and_wait_current_download(&self) {
|
fn stop_and_wait_current_download(&self) {
|
||||||
self.set_status(DownloadManagerStatus::Paused).await;
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
if let Some(current_flag) = &self.active_control_flag {
|
if let Some(current_flag) = &self.active_control_flag {
|
||||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||||
current_download_thread.await.unwrap();
|
current_download_thread.join().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn manage_queue(mut self) -> Result<(), ()> {
|
fn manage_queue(mut self) -> Result<(), ()> {
|
||||||
loop {
|
loop {
|
||||||
let signal = match self.command_receiver.lock().await.recv() {
|
let signal = match self.command_receiver.recv() {
|
||||||
Ok(signal) => signal,
|
Ok(signal) => signal,
|
||||||
Err(_) => return Err(()),
|
Err(_) => return Err(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match signal {
|
match signal {
|
||||||
DownloadManagerSignal::Go => {
|
DownloadManagerSignal::Go => {
|
||||||
self.manage_go_signal().await;
|
self.manage_go_signal();
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Stop => {
|
DownloadManagerSignal::Stop => {
|
||||||
self.manage_stop_signal().await;
|
self.manage_stop_signal();
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Completed(meta) => {
|
DownloadManagerSignal::Completed(meta) => {
|
||||||
self.manage_completed_signal(meta).await;
|
self.manage_completed_signal(meta);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Queue(download_agent) => {
|
DownloadManagerSignal::Queue(download_agent) => {
|
||||||
self.manage_queue_signal(download_agent).await;
|
self.manage_queue_signal(download_agent);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Error(e) => {
|
DownloadManagerSignal::Error(e) => {
|
||||||
self.manage_error_signal(e).await;
|
self.manage_error_signal(e);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::UpdateUIQueue => {
|
DownloadManagerSignal::UpdateUIQueue => {
|
||||||
self.push_ui_queue_update().await;
|
self.push_ui_queue_update();
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
||||||
self.push_ui_stats_update(kbs, time);
|
self.push_ui_stats_update(kbs, time);
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Finish => {
|
DownloadManagerSignal::Finish => {
|
||||||
self.stop_and_wait_current_download().await;
|
self.stop_and_wait_current_download();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
DownloadManagerSignal::Cancel(meta) => {
|
DownloadManagerSignal::Cancel(meta) => {
|
||||||
self.manage_cancel_signal(&meta).await;
|
self.manage_cancel_signal(&meta);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||||
debug!("got signal Queue");
|
debug!("got signal Queue");
|
||||||
let meta = download_agent.metadata();
|
let meta = download_agent.metadata();
|
||||||
|
|
||||||
debug!("queue metadata: {meta:?}");
|
debug!("queue metadata: {:?}", meta);
|
||||||
|
|
||||||
if self.download_queue.exists(meta.clone()) {
|
if self.download_queue.exists(meta.clone()) {
|
||||||
warn!("download with same ID already exists");
|
warn!("download with same ID already exists");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
download_agent.on_initialised(&self.app_handle).await;
|
download_agent.on_initialised(&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);
|
||||||
|
|
||||||
@ -214,7 +195,7 @@ impl DownloadManagerBuilder {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn manage_go_signal(&mut self) {
|
fn manage_go_signal(&mut self) {
|
||||||
debug!("got signal Go");
|
debug!("got signal Go");
|
||||||
if self.download_agent_registry.is_empty() {
|
if self.download_agent_registry.is_empty() {
|
||||||
debug!(
|
debug!(
|
||||||
@ -224,8 +205,8 @@ impl DownloadManagerBuilder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.current_download_agent.is_some()
|
if self.current_download_agent.is_some() {
|
||||||
&& self.download_queue.read().front().unwrap()
|
if self.download_queue.read().front().unwrap()
|
||||||
== &self.current_download_agent.as_ref().unwrap().metadata()
|
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
{
|
{
|
||||||
debug!(
|
debug!(
|
||||||
@ -234,13 +215,14 @@ impl DownloadManagerBuilder {
|
|||||||
);
|
);
|
||||||
return;
|
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
|
// Should always be Some if the above two statements keep going
|
||||||
let agent_data = self.download_queue.read().front().unwrap().clone();
|
let agent_data = self.download_queue.read().front().unwrap().clone();
|
||||||
|
|
||||||
info!("starting download for {agent_data:?}");
|
info!("starting download for {:?}", agent_data);
|
||||||
|
|
||||||
let download_agent = self
|
let download_agent = self
|
||||||
.download_agent_registry
|
.download_agent_registry
|
||||||
@ -248,117 +230,88 @@ impl DownloadManagerBuilder {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
self.active_control_flag = Some(download_agent.control_flag().await);
|
self.active_control_flag = Some(download_agent.control_flag());
|
||||||
self.current_download_agent = Some(download_agent.clone());
|
self.current_download_agent = Some(download_agent.clone());
|
||||||
|
|
||||||
info!("fetched control flag");
|
|
||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
info!("cloned sender");
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
|
||||||
info!("acquired download thread lock");
|
|
||||||
let app_handle = self.app_handle.clone();
|
let app_handle = self.app_handle.clone();
|
||||||
|
|
||||||
info!("starting download agent thread");
|
*download_thread_lock = Some(spawn(move || {
|
||||||
|
match download_agent.download(&app_handle) {
|
||||||
*download_thread_lock = Some(self.runtime.spawn(async move {
|
|
||||||
info!("started download agent thread");
|
|
||||||
match download_agent.download(&app_handle).await {
|
|
||||||
// Ok(true) is for completed and exited properly
|
// Ok(true) is for completed and exited properly
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
debug!("download {:?} has completed", download_agent.metadata());
|
debug!("download {:?} has completed", download_agent.metadata());
|
||||||
match download_agent.validate().await {
|
download_agent.on_complete(&app_handle);
|
||||||
Ok(true) => {
|
|
||||||
download_agent.on_complete(&app_handle).await;
|
|
||||||
sender
|
sender
|
||||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
|
||||||
download_agent.on_incomplete(&app_handle).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"download {:?} has validation error {}",
|
|
||||||
download_agent.metadata(),
|
|
||||||
&e
|
|
||||||
);
|
|
||||||
download_agent.on_error(&app_handle, &e).await;
|
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ok(false) is for incomplete but exited properly
|
// Ok(false) is for incomplete but exited properly
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
debug!("Donwload agent finished incomplete");
|
download_agent.on_incomplete(&app_handle);
|
||||||
download_agent.on_incomplete(&app_handle).await;
|
|
||||||
}
|
}
|
||||||
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).await;
|
download_agent.on_error(&app_handle, &e);
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.set_status(DownloadManagerStatus::Downloading).await;
|
self.set_status(DownloadManagerStatus::Downloading);
|
||||||
let active_control_flag = self.active_control_flag.clone().unwrap();
|
let active_control_flag = self.active_control_flag.clone().unwrap();
|
||||||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||||
|
|
||||||
// download_thread_lock.take().unwrap().await;
|
|
||||||
}
|
}
|
||||||
async fn manage_stop_signal(&mut self) {
|
fn manage_stop_signal(&mut self) {
|
||||||
debug!("got signal Stop");
|
debug!("got signal Stop");
|
||||||
|
|
||||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||||
self.set_status(DownloadManagerStatus::Paused).await;
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async 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.current_download_agent {
|
||||||
&& interface.metadata() == meta
|
if interface.metadata() == meta {
|
||||||
{
|
self.remove_and_cleanup_front_download(&meta);
|
||||||
self.remove_and_cleanup_front_download(&meta).await;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.push_ui_queue_update().await;
|
self.push_ui_queue_update();
|
||||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||||
}
|
}
|
||||||
async 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(current_agent) = self.current_download_agent.clone() {
|
||||||
current_agent.on_error(&self.app_handle, &error).await;
|
current_agent.on_error(&self.app_handle, &error);
|
||||||
|
|
||||||
self.stop_and_wait_current_download().await;
|
self.stop_and_wait_current_download();
|
||||||
self.remove_and_cleanup_front_download(¤t_agent.metadata())
|
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
self.set_status(DownloadManagerStatus::Error).await;
|
self.set_status(DownloadManagerStatus::Error(error));
|
||||||
}
|
}
|
||||||
async 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 let Some(current_download) = &self.current_download_agent {
|
||||||
if ¤t_download.metadata() == meta {
|
if ¤t_download.metadata() == meta {
|
||||||
self.set_status(DownloadManagerStatus::Paused).await;
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
current_download.on_cancelled(&self.app_handle).await;
|
current_download.on_cancelled(&self.app_handle);
|
||||||
self.stop_and_wait_current_download().await;
|
self.stop_and_wait_current_download();
|
||||||
|
|
||||||
self.download_queue.pop_front();
|
self.download_queue.pop_front();
|
||||||
|
|
||||||
self.cleanup_current_download().await;
|
self.cleanup_current_download();
|
||||||
debug!("current download queue: {:?}", self.download_queue.read());
|
debug!("current download queue: {:?}", self.download_queue.read());
|
||||||
}
|
}
|
||||||
// TODO: Collapse these two into a single if statement somehow
|
// TODO: Collapse these two into a single if statement somehow
|
||||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||||
let index = self.download_queue.get_by_meta(meta);
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
download_agent.on_cancelled(&self.app_handle).await;
|
download_agent.on_cancelled(&self.app_handle);
|
||||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||||
let removed = self.download_agent_registry.remove(meta);
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
debug!(
|
debug!(
|
||||||
@ -371,7 +324,7 @@ impl DownloadManagerBuilder {
|
|||||||
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||||
let index = self.download_queue.get_by_meta(meta);
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
download_agent.on_cancelled(&self.app_handle).await;
|
download_agent.on_cancelled(&self.app_handle);
|
||||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||||
let removed = self.download_agent_registry.remove(meta);
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
debug!(
|
debug!(
|
||||||
@ -381,26 +334,28 @@ impl DownloadManagerBuilder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_ui_queue_update().await;
|
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();
|
self.app_handle.emit("update_stats", event_data).unwrap();
|
||||||
}
|
}
|
||||||
async fn push_ui_queue_update(&self) {
|
fn push_ui_queue_update(&self) {
|
||||||
let queue = &self.download_queue.read();
|
let queue = &self.download_queue.read();
|
||||||
let queue_objs = join_all(queue.iter().map(async |key| {
|
let queue_objs = queue
|
||||||
|
.iter()
|
||||||
|
.map(|key| {
|
||||||
let val = self.download_agent_registry.get(key).unwrap();
|
let val = self.download_agent_registry.get(key).unwrap();
|
||||||
QueueUpdateEventQueueData {
|
QueueUpdateEventQueueData {
|
||||||
meta: DownloadableMetadata::clone(key),
|
meta: DownloadableMetadata::clone(key),
|
||||||
status: val.status().await,
|
status: val.status(),
|
||||||
progress: val.progress().await.get_progress(),
|
progress: val.progress().get_progress(),
|
||||||
current: val.progress().await.sum(),
|
current: val.progress().sum(),
|
||||||
max: val.progress().await.get_max(),
|
max: val.progress().get_max(),
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
.await;
|
.collect();
|
||||||
|
|
||||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||||
|
|||||||
@ -8,21 +8,18 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_frontend::DownloadStatus,
|
download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||||
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait Downloadable: Send + Sync {
|
pub trait Downloadable: Send + Sync {
|
||||||
async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||||
async fn progress(&self) -> Arc<ProgressObject>;
|
fn progress(&self) -> Arc<ProgressObject>;
|
||||||
async fn control_flag(&self) -> DownloadThreadControl;
|
fn control_flag(&self) -> DownloadThreadControl;
|
||||||
async fn validate(&self) -> Result<bool, ApplicationDownloadError>;
|
fn status(&self) -> DownloadStatus;
|
||||||
async fn status(&self) -> DownloadStatus;
|
|
||||||
fn metadata(&self) -> DownloadableMetadata;
|
fn metadata(&self) -> DownloadableMetadata;
|
||||||
async fn on_initialised(&self, app_handle: &AppHandle);
|
fn on_initialised(&self, app_handle: &AppHandle);
|
||||||
async fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
||||||
async fn on_complete(&self, app_handle: &AppHandle);
|
fn on_complete(&self, app_handle: &AppHandle);
|
||||||
async fn on_incomplete(&self, app_handle: &AppHandle);
|
fn on_incomplete(&self, app_handle: &AppHandle);
|
||||||
async fn on_cancelled(&self, app_handle: &AppHandle);
|
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod download_manager;
|
||||||
pub mod download_manager_builder;
|
pub mod download_manager_builder;
|
||||||
pub mod download_manager_frontend;
|
|
||||||
pub mod downloadable;
|
pub mod downloadable;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
pub mod download_thread_control_flag;
|
|
||||||
pub mod progress_object;
|
pub mod progress_object;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod rolling_progress_updates;
|
pub mod rolling_progress_updates;
|
||||||
|
pub mod download_thread_control_flag;
|
||||||
@ -10,9 +10,11 @@ use std::{
|
|||||||
use atomic_instant_full::AtomicInstant;
|
use atomic_instant_full::AtomicInstant;
|
||||||
use throttle_my_fn::throttle;
|
use throttle_my_fn::throttle;
|
||||||
|
|
||||||
use crate::download_manager::download_manager_frontend::DownloadManagerSignal;
|
use crate::download_manager::download_manager::DownloadManagerSignal;
|
||||||
|
|
||||||
use super::rolling_progress_updates::RollingProgressWindow;
|
use super::{
|
||||||
|
rolling_progress_updates::RollingProgressWindow,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ProgressObject {
|
pub struct ProgressObject {
|
||||||
@ -84,17 +86,6 @@ impl ProgressObject {
|
|||||||
.map(|instance| instance.load(Ordering::Relaxed))
|
.map(|instance| instance.load(Ordering::Relaxed))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
pub fn reset(&self, size: usize) {
|
|
||||||
self.set_time_now();
|
|
||||||
self.set_size(size);
|
|
||||||
self.bytes_last_update.store(0, Ordering::Release);
|
|
||||||
self.rolling.reset();
|
|
||||||
self.progress_instances
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.for_each(|x| x.store(0, Ordering::Release));
|
|
||||||
}
|
|
||||||
pub fn get_max(&self) -> usize {
|
pub fn get_max(&self) -> usize {
|
||||||
*self.max.lock().unwrap()
|
*self.max.lock().unwrap()
|
||||||
}
|
}
|
||||||
@ -133,7 +124,7 @@ pub fn calculate_update(progress: &ProgressObject) {
|
|||||||
|
|
||||||
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 as usize).max(1);
|
||||||
|
|
||||||
let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes
|
let bytes_remaining = max - current_bytes_downloaded; // bytes
|
||||||
|
|
||||||
progress.update_window(kilobytes_per_second);
|
progress.update_window(kilobytes_per_second);
|
||||||
push_update(progress, bytes_remaining);
|
push_update(progress, bytes_remaining);
|
||||||
|
|||||||
@ -32,13 +32,49 @@ impl Queue {
|
|||||||
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
|
||||||
self.edit().pop_front()
|
self.edit().pop_front()
|
||||||
}
|
}
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.inner.lock().unwrap().len() == 0
|
||||||
|
}
|
||||||
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
|
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
|
||||||
self.read().contains(&meta)
|
self.read().contains(&meta)
|
||||||
}
|
}
|
||||||
|
/// Either inserts `interface` at the specified index, or appends to
|
||||||
|
/// the back of the deque if index is greater than the length of the deque
|
||||||
|
pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
|
||||||
|
if self.read().len() > index {
|
||||||
|
self.append(interface);
|
||||||
|
} else {
|
||||||
|
self.edit().insert(index, interface);
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn append(&self, interface: DownloadableMetadata) {
|
pub fn append(&self, interface: DownloadableMetadata) {
|
||||||
self.edit().push_back(interface);
|
self.edit().push_back(interface);
|
||||||
}
|
}
|
||||||
|
pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option<DownloadableMetadata> {
|
||||||
|
let mut queue = self.edit();
|
||||||
|
let front = queue.front()?;
|
||||||
|
if front == meta {
|
||||||
|
return queue.pop_front();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option<usize> {
|
pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option<usize> {
|
||||||
self.read().iter().position(|data| data == meta)
|
self.read().iter().position(|data| data == meta)
|
||||||
}
|
}
|
||||||
|
pub fn move_to_index_by_meta(
|
||||||
|
&self,
|
||||||
|
meta: &DownloadableMetadata,
|
||||||
|
new_index: usize,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let index = match self.get_by_meta(meta) {
|
||||||
|
Some(index) => index,
|
||||||
|
None => return Err(()),
|
||||||
|
};
|
||||||
|
let existing = match self.edit().remove(index) {
|
||||||
|
Some(existing) => existing,
|
||||||
|
None => return Err(()),
|
||||||
|
};
|
||||||
|
self.edit().insert(new_index, existing);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,9 +30,4 @@ impl<const S: usize> RollingProgressWindow<S> {
|
|||||||
.sum::<usize>()
|
.sum::<usize>()
|
||||||
/ S
|
/ S
|
||||||
}
|
}
|
||||||
pub fn reset(&self) {
|
|
||||||
self.window
|
|
||||||
.iter()
|
|
||||||
.for_each(|x| x.store(0, Ordering::Release));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,14 @@ use std::{
|
|||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
use super::remote_access_error::RemoteAccessError;
|
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
|
||||||
|
|
||||||
// TODO: Rename / separate from downloads
|
// TODO: Rename / separate from downloads
|
||||||
#[derive(Debug, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum ApplicationDownloadError {
|
pub enum ApplicationDownloadError {
|
||||||
Communication(RemoteAccessError),
|
Communication(RemoteAccessError),
|
||||||
Checksum,
|
Checksum,
|
||||||
|
Setup(SetupError),
|
||||||
Lock,
|
Lock,
|
||||||
IoError(io::ErrorKind),
|
IoError(io::ErrorKind),
|
||||||
DownloadError,
|
DownloadError,
|
||||||
@ -20,10 +21,11 @@ pub enum ApplicationDownloadError {
|
|||||||
impl Display for ApplicationDownloadError {
|
impl Display for ApplicationDownloadError {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
|
ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
|
||||||
|
ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
|
||||||
ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
|
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::Checksum => write!(f, "checksum failed to validate for download"),
|
||||||
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
|
||||||
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
|
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src-tauri/src/error/backup_error.rs
Normal file
21
src-tauri/src/error/backup_error.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,8 +10,8 @@ pub enum DownloadManagerError<T> {
|
|||||||
impl<T> Display for DownloadManagerError<T> {
|
impl<T> Display for DownloadManagerError<T> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
DownloadManagerError::IOError(error) => write!(f, "{error}"),
|
DownloadManagerError::IOError(error) => write!(f, "{}", error),
|
||||||
DownloadManagerError::SignalError(send_error) => write!(f, "{send_error}"),
|
DownloadManagerError::SignalError(send_error) => write!(f, "{}", send_error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,6 @@ use serde::Deserialize;
|
|||||||
pub struct DropServerError {
|
pub struct DropServerError {
|
||||||
pub status_code: usize,
|
pub status_code: usize,
|
||||||
pub status_message: String,
|
pub status_message: String,
|
||||||
// pub message: String,
|
pub message: String,
|
||||||
// pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,8 @@ impl Display for LibraryError {
|
|||||||
match self {
|
match self {
|
||||||
LibraryError::MetaNotFound(id) => write!(
|
LibraryError::MetaNotFound(id) => write!(
|
||||||
f,
|
f,
|
||||||
"Could not locate any installed version of game ID {id} in the database"
|
"Could not locate any installed version of game ID {} in the database",
|
||||||
|
id
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
pub mod application_download_error;
|
pub mod application_download_error;
|
||||||
pub mod download_manager_error;
|
|
||||||
pub mod drop_server_error;
|
pub mod drop_server_error;
|
||||||
|
pub mod download_manager_error;
|
||||||
pub mod library_error;
|
pub mod library_error;
|
||||||
pub mod process_error;
|
pub mod process_error;
|
||||||
pub mod remote_access_error;
|
pub mod remote_access_error;
|
||||||
|
pub mod setup_error;
|
||||||
|
pub mod backup_error;
|
||||||
@ -13,7 +13,6 @@ pub enum ProcessError {
|
|||||||
IOError(Error),
|
IOError(Error),
|
||||||
FormatError(String), // String errors supremacy
|
FormatError(String), // String errors supremacy
|
||||||
InvalidPlatform,
|
InvalidPlatform,
|
||||||
OpenerError(tauri_plugin_opener::Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ProcessError {
|
impl Display for ProcessError {
|
||||||
@ -23,13 +22,12 @@ impl Display for ProcessError {
|
|||||||
ProcessError::NotInstalled => "Game not installed",
|
ProcessError::NotInstalled => "Game not installed",
|
||||||
ProcessError::AlreadyRunning => "Game already running",
|
ProcessError::AlreadyRunning => "Game already running",
|
||||||
ProcessError::NotDownloaded => "Game not downloaded",
|
ProcessError::NotDownloaded => "Game not downloaded",
|
||||||
ProcessError::InvalidID => "Invalid game ID",
|
ProcessError::InvalidID => "Invalid Game ID",
|
||||||
ProcessError::InvalidVersion => "Invalid game version",
|
ProcessError::InvalidVersion => "Invalid Game version",
|
||||||
ProcessError::IOError(error) => &error.to_string(),
|
ProcessError::IOError(error) => &error.to_string(),
|
||||||
ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
|
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
|
||||||
ProcessError::FormatError(e) => &format!("Failed to format template: {e}"),
|
ProcessError::FormatError(e) => &format!("Failed to format template: {}", e),
|
||||||
ProcessError::OpenerError(error) => &format!("Failed to open directory: {error}"),
|
|
||||||
};
|
};
|
||||||
write!(f, "{s}")
|
write!(f, "{}", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ pub enum RemoteAccessError {
|
|||||||
HandshakeFailed(String),
|
HandshakeFailed(String),
|
||||||
GameNotFound(String),
|
GameNotFound(String),
|
||||||
InvalidResponse(DropServerError),
|
InvalidResponse(DropServerError),
|
||||||
UnparseableResponse(String),
|
InvalidRedirect,
|
||||||
ManifestDownloadFailed(StatusCode, String),
|
ManifestDownloadFailed(StatusCode, String),
|
||||||
OutOfSync,
|
OutOfSync,
|
||||||
Cache(cacache::Error),
|
Cache(cacache::Error),
|
||||||
@ -44,19 +44,20 @@ impl Display for RemoteAccessError {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
RemoteAccessError::ParsingError(parse_error) => {
|
RemoteAccessError::ParsingError(parse_error) => {
|
||||||
write!(f, "{parse_error}")
|
write!(f, "{}", parse_error)
|
||||||
}
|
}
|
||||||
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
|
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
|
||||||
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {message}"),
|
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
|
||||||
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {id}"),
|
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {}", id),
|
||||||
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {}, {}", error.status_code, error.status_message),
|
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
|
||||||
RemoteAccessError::UnparseableResponse(error) => write!(f, "server returned an invalid response: {error}"),
|
RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
|
||||||
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
|
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
|
||||||
f,
|
f,
|
||||||
"failed to download game manifest: {status} {response}"
|
"failed to download game manifest: {} {}",
|
||||||
|
status, response
|
||||||
),
|
),
|
||||||
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
|
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
|
||||||
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {error}"),
|
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src-tauri/src/error/setup_error.rs
Normal file
14
src-tauri/src/error/setup_error.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SetupError {
|
||||||
|
Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SetupError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SetupError::Context => write!(f, "failed to generate contexts for download"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,116 +1,110 @@
|
|||||||
use reqwest::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
DB,
|
|
||||||
database::db::DatabaseImpls,
|
database::db::DatabaseImpls,
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::remote_access_error::RemoteAccessError,
|
||||||
remote::{auth::generate_authorization_header, requests::make_request},
|
remote::{auth::generate_authorization_header, requests::make_request},
|
||||||
|
DB,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::collection::{Collection, Collections};
|
use super::collection::{Collection, Collections};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let response = make_request(&client, &["/api/v1/client/collection"], &[], async |r| {
|
let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
|
||||||
r.header("Authorization", generate_authorization_header().await)
|
r.header("Authorization", generate_authorization_header())
|
||||||
})
|
})?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(response.json().await?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let response = make_request(
|
let response = make_request(
|
||||||
&client,
|
&client,
|
||||||
&["/api/v1/client/collection/", &collection_id],
|
&["/api/v1/client/collection/", &collection_id],
|
||||||
&[],
|
&[],
|
||||||
async |r| r.header("Authorization", generate_authorization_header().await),
|
|r| r.header("Authorization", generate_authorization_header()),
|
||||||
)
|
)?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(response.json().await?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let base_url = DB.fetch_base_url().await;
|
let base_url = DB.fetch_base_url();
|
||||||
|
|
||||||
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
|
let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?;
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.post(base_url)
|
.post(base_url)
|
||||||
.header("Authorization", generate_authorization_header().await)
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"name": name}))
|
.json(&json!({"name": name}))
|
||||||
.send()
|
.send()?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(response.json().await?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn add_game_to_collection(
|
pub fn add_game_to_collection(
|
||||||
collection_id: String,
|
collection_id: String,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let url = Url::parse(&format!(
|
let url = Url::parse(&format!(
|
||||||
"{}api/v1/client/collection/{}/entry/",
|
"{}api/v1/client/collection/{}/entry/",
|
||||||
DB.fetch_base_url().await,
|
DB.fetch_base_url(),
|
||||||
collection_id
|
collection_id
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("Authorization", generate_authorization_header().await)
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"id": game_id}))
|
.json(&json!({"id": game_id}))
|
||||||
.send()
|
.send()?;
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_collection(collection_id: String) -> Result<(), RemoteAccessError> {
|
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let base_url = Url::parse(&format!(
|
let base_url = Url::parse(&format!(
|
||||||
"{}api/v1/client/collection/{}",
|
"{}api/v1/client/collection/{}",
|
||||||
DB.fetch_base_url().await,
|
DB.fetch_base_url(),
|
||||||
collection_id
|
collection_id
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
client
|
let response = client
|
||||||
.delete(base_url)
|
.delete(base_url)
|
||||||
.header("Authorization", generate_authorization_header().await)
|
.header("Authorization", generate_authorization_header())
|
||||||
.send().await?;
|
.send()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_game_in_collection(
|
pub fn delete_game_in_collection(
|
||||||
collection_id: String,
|
collection_id: String,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let base_url = Url::parse(&format!(
|
let base_url = Url::parse(&format!(
|
||||||
"{}api/v1/client/collection/{}/entry",
|
"{}api/v1/client/collection/{}/entry",
|
||||||
DB.fetch_base_url().await,
|
DB.fetch_base_url(),
|
||||||
collection_id
|
collection_id
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.delete(base_url)
|
.delete(base_url)
|
||||||
.header("Authorization", generate_authorization_header().await)
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"id": game_id}))
|
.json(&json!({"id": game_id}))
|
||||||
.send().await?;
|
.send()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
use serde::Deserialize;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{db::borrow_db_mut_checked, models::data::GameVersion}, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{
|
database::models::data::GameVersion,
|
||||||
|
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
|
||||||
|
games::library::{
|
||||||
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
|
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
|
||||||
uninstall_game_logic,
|
uninstall_game_logic,
|
||||||
}, offline, DropFunctionState
|
},
|
||||||
|
offline, AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
library::{
|
library::{
|
||||||
FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic,
|
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
|
||||||
fetch_library_logic,
|
Game,
|
||||||
},
|
},
|
||||||
state::{GameStatusManager, GameStatusWithTransient},
|
state::{GameStatusManager, GameStatusWithTransient},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_library(
|
pub fn fetch_library(
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
@ -26,13 +30,12 @@ pub async fn fetch_library(
|
|||||||
fetch_library_logic_offline,
|
fetch_library_logic_offline,
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_game(
|
pub fn fetch_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
@ -41,74 +44,28 @@ pub async fn fetch_game(
|
|||||||
game_id,
|
game_id,
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||||
GameStatusManager::fetch_state(&id).await
|
GameStatusManager::fetch_state(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
||||||
let meta = match get_current_meta(&game_id).await {
|
let meta = match get_current_meta(&game_id) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return Err(LibraryError::MetaNotFound(game_id)),
|
None => return Err(LibraryError::MetaNotFound(game_id)),
|
||||||
};
|
};
|
||||||
uninstall_game_logic(meta, &app_handle).await;
|
uninstall_game_logic(meta, &app_handle);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_game_verion_options(
|
pub fn fetch_game_verion_options(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||||
fetch_game_verion_options_logic(game_id, state).await
|
fetch_game_verion_options_logic(game_id, state)
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct FrontendGameOptions {
|
|
||||||
launch_string: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn update_game_configuration(
|
|
||||||
game_id: String,
|
|
||||||
options: FrontendGameOptions,
|
|
||||||
) -> Result<(), LibraryError> {
|
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
|
||||||
let installed_version = handle
|
|
||||||
.applications
|
|
||||||
.installed_game_version
|
|
||||||
.get(&game_id)
|
|
||||||
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
|
||||||
|
|
||||||
let id = installed_version.id.clone();
|
|
||||||
let version = installed_version.version.clone().unwrap();
|
|
||||||
|
|
||||||
let mut existing_configuration = handle
|
|
||||||
.applications
|
|
||||||
.game_versions
|
|
||||||
.get(&id)
|
|
||||||
.unwrap()
|
|
||||||
.get(&version)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
// Add more options in here
|
|
||||||
existing_configuration.launch_command_template = options.launch_string;
|
|
||||||
|
|
||||||
// Add no more options past here
|
|
||||||
|
|
||||||
handle
|
|
||||||
.applications
|
|
||||||
.game_versions
|
|
||||||
.get_mut(&id)
|
|
||||||
.unwrap()
|
|
||||||
.insert(version.to_string(), existing_configuration);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,71 +1,30 @@
|
|||||||
use std::{
|
use std::sync::{Arc, Mutex};
|
||||||
path::PathBuf,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
|
|
||||||
download_manager::{
|
download_manager::{
|
||||||
download_manager_frontend::DownloadManagerSignal, downloadable::Downloadable,
|
download_manager::DownloadManagerSignal, downloadable::Downloadable,
|
||||||
},
|
}, error::download_manager_error::DownloadManagerError, AppState
|
||||||
error::download_manager_error::DownloadManagerError, DropFunctionState,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::download_agent::GameDownloadAgent;
|
use super::download_agent::GameDownloadAgent;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_game(
|
pub fn download_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
game_version: String,
|
game_version: String,
|
||||||
install_dir: usize,
|
install_dir: usize,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||||
let sender = state.lock().await.download_manager.get_sender();
|
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
|
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||||
game_id,
|
game_id,
|
||||||
game_version,
|
game_version,
|
||||||
install_dir,
|
install_dir,
|
||||||
sender,
|
sender,
|
||||||
).await) as Box<dyn Downloadable + Send + Sync>);
|
|
||||||
Ok(state
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.download_manager
|
|
||||||
.queue_download(game_download_agent)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn resume_download(
|
|
||||||
game_id: String,
|
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
|
||||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
|
||||||
let s = borrow_db_checked().await
|
|
||||||
.applications
|
|
||||||
.game_statuses
|
|
||||||
.get(&game_id)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let (version_name, install_dir) = match s {
|
|
||||||
GameDownloadStatus::Remote {} => unreachable!(),
|
|
||||||
GameDownloadStatus::SetupRequired { .. } => unreachable!(),
|
|
||||||
GameDownloadStatus::Installed { .. } => unreachable!(),
|
|
||||||
GameDownloadStatus::PartiallyInstalled {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
} => (version_name, install_dir),
|
|
||||||
};
|
|
||||||
let sender = state.lock().await.download_manager.get_sender();
|
|
||||||
let parent_dir: PathBuf = install_dir.into();
|
|
||||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
|
||||||
game_id,
|
|
||||||
version_name.clone(),
|
|
||||||
parent_dir.parent().unwrap().to_path_buf(),
|
|
||||||
sender,
|
|
||||||
)) as Box<dyn Downloadable + Send + Sync>);
|
)) as Box<dyn Downloadable + Send + Sync>);
|
||||||
Ok(state
|
Ok(state
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.unwrap()
|
||||||
.download_manager
|
.download_manager
|
||||||
.queue_download(game_download_agent)?)
|
.queue_download(game_download_agent)?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,83 +1,66 @@
|
|||||||
use crate::DB;
|
|
||||||
use crate::auth::generate_authorization_header;
|
use crate::auth::generate_authorization_header;
|
||||||
use crate::database::db::{DatabaseImpls, borrow_db_checked, borrow_db_mut_checked};
|
use crate::database::db::borrow_db_checked;
|
||||||
use crate::database::models::data::{
|
use crate::database::models::data::{
|
||||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata,
|
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||||
};
|
};
|
||||||
use crate::download_manager::download_manager_frontend::{DownloadManagerSignal, DownloadStatus};
|
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||||
use crate::download_manager::downloadable::Downloadable;
|
use crate::download_manager::downloadable::Downloadable;
|
||||||
use crate::download_manager::util::download_thread_control_flag::{
|
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||||
DownloadThreadControl, DownloadThreadControlFlag,
|
|
||||||
};
|
|
||||||
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||||
use crate::games::downloads::validate::game_validate_logic;
|
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
|
||||||
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
|
|
||||||
use crate::remote::requests::make_request;
|
use crate::remote::requests::make_request;
|
||||||
use log::{debug, error, info, warn};
|
use crate::DB;
|
||||||
use std::collections::HashMap;
|
use log::{debug, error, info};
|
||||||
use std::fs::{OpenOptions, create_dir_all};
|
use rayon::ThreadPoolBuilder;
|
||||||
use std::path::{Path, PathBuf};
|
use slice_deque::SliceDeque;
|
||||||
|
use std::fs::{create_dir_all, File};
|
||||||
|
use std::path::Path;
|
||||||
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, Emitter};
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use rustix::fs::{FallocateFlags, fallocate};
|
use rustix::fs::{fallocate, FallocateFlags};
|
||||||
|
|
||||||
use super::download_logic::download_game_chunk;
|
use super::download_logic::download_game_chunk;
|
||||||
use super::drop_data::DropData;
|
use super::stored_manifest::StoredManifest;
|
||||||
|
|
||||||
// This is cursed but necessary
|
|
||||||
// See the message where it is used
|
|
||||||
unsafe fn extend_lifetime<'b, R>(r: &'b R) -> &'static R {
|
|
||||||
unsafe { std::mem::transmute::<&'b R, &'static R>(r) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GameDownloadAgent {
|
pub struct GameDownloadAgent {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub control_flag: DownloadThreadControl,
|
pub control_flag: DownloadThreadControl,
|
||||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||||
context_map: Mutex<HashMap<String, bool>>,
|
completed_contexts: Mutex<SliceDeque<usize>>,
|
||||||
pub manifest: Mutex<Option<DropManifest>>,
|
pub manifest: Mutex<Option<DropManifest>>,
|
||||||
pub progress: Arc<ProgressObject>,
|
pub progress: Arc<ProgressObject>,
|
||||||
sender: Sender<DownloadManagerSignal>,
|
sender: Sender<DownloadManagerSignal>,
|
||||||
pub stored_manifest: DropData,
|
pub stored_manifest: StoredManifest,
|
||||||
status: Mutex<DownloadStatus>,
|
status: Mutex<DownloadStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameDownloadAgent {
|
impl GameDownloadAgent {
|
||||||
pub async fn new_from_index(
|
pub fn new(
|
||||||
id: String,
|
id: String,
|
||||||
version: String,
|
version: String,
|
||||||
target_download_dir: usize,
|
target_download_dir: usize,
|
||||||
sender: Sender<DownloadManagerSignal>,
|
sender: Sender<DownloadManagerSignal>,
|
||||||
) -> Self {
|
|
||||||
let db_lock = borrow_db_checked().await;
|
|
||||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
|
||||||
drop(db_lock);
|
|
||||||
|
|
||||||
Self::new(id, version, base_dir, sender)
|
|
||||||
}
|
|
||||||
pub fn new(
|
|
||||||
id: String,
|
|
||||||
version: String,
|
|
||||||
base_dir: PathBuf,
|
|
||||||
sender: Sender<DownloadManagerSignal>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Don't run by default
|
// Don't run by default
|
||||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||||
|
|
||||||
|
let db_lock = borrow_db_checked();
|
||||||
|
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||||
|
drop(db_lock);
|
||||||
|
|
||||||
let base_dir_path = Path::new(&base_dir);
|
let base_dir_path = Path::new(&base_dir);
|
||||||
let data_base_dir_path = base_dir_path.join(id.clone());
|
let data_base_dir_path = base_dir_path.join(id.clone());
|
||||||
|
|
||||||
let stored_manifest =
|
let stored_manifest =
|
||||||
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@ -85,7 +68,7 @@ impl GameDownloadAgent {
|
|||||||
control_flag,
|
control_flag,
|
||||||
manifest: Mutex::new(None),
|
manifest: Mutex::new(None),
|
||||||
contexts: Mutex::new(Vec::new()),
|
contexts: Mutex::new(Vec::new()),
|
||||||
context_map: Mutex::new(HashMap::new()),
|
completed_contexts: Mutex::new(SliceDeque::new()),
|
||||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||||
sender,
|
sender,
|
||||||
stored_manifest,
|
stored_manifest,
|
||||||
@ -94,8 +77,8 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blocking
|
// Blocking
|
||||||
pub async fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
self.ensure_manifest_exists().await?;
|
self.ensure_manifest_exists()?;
|
||||||
|
|
||||||
self.ensure_contexts()?;
|
self.ensure_contexts()?;
|
||||||
|
|
||||||
@ -105,9 +88,8 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blocking
|
// Blocking
|
||||||
pub async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
debug!("starting download");
|
self.setup_download()?;
|
||||||
self.setup_download().await?;
|
|
||||||
self.set_progress_object_params();
|
self.set_progress_object_params();
|
||||||
let timer = Instant::now();
|
let timer = Instant::now();
|
||||||
push_game_update(
|
push_game_update(
|
||||||
@ -123,7 +105,6 @@ impl GameDownloadAgent {
|
|||||||
);
|
);
|
||||||
let res = self
|
let res = self
|
||||||
.run()
|
.run()
|
||||||
.await
|
|
||||||
.map_err(|_| ApplicationDownloadError::DownloadError);
|
.map_err(|_| ApplicationDownloadError::DownloadError);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
@ -134,39 +115,37 @@ impl GameDownloadAgent {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
if self.manifest.lock().unwrap().is_some() {
|
if self.manifest.lock().unwrap().is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.download_manifest().await
|
self.download_manifest()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
let header = generate_authorization_header().await;
|
let header = generate_authorization_header();
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = make_request(
|
let response = make_request(
|
||||||
&client,
|
&client,
|
||||||
&["/api/v1/client/game/manifest"],
|
&["/api/v1/client/game/manifest"],
|
||||||
&[("id", &self.id), ("version", &self.version)],
|
&[("id", &self.id), ("version", &self.version)],
|
||||||
async |f| f.header("Authorization", header),
|
|f| f.header("Authorization", header),
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.map_err(ApplicationDownloadError::Communication)?
|
.map_err(ApplicationDownloadError::Communication)?
|
||||||
.send()
|
.send()
|
||||||
.await
|
|
||||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
return Err(ApplicationDownloadError::Communication(
|
return Err(ApplicationDownloadError::Communication(
|
||||||
RemoteAccessError::ManifestDownloadFailed(
|
RemoteAccessError::ManifestDownloadFailed(
|
||||||
response.status(),
|
response.status(),
|
||||||
response.text().await.unwrap(),
|
response.text().unwrap(),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest_download: DropManifest = response.json().await.unwrap();
|
let manifest_download: DropManifest = response.json().unwrap();
|
||||||
|
|
||||||
if let Ok(mut manifest) = self.manifest.lock() {
|
if let Ok(mut manifest) = self.manifest.lock() {
|
||||||
*manifest = Some(manifest_download);
|
*manifest = Some(manifest_download);
|
||||||
@ -194,15 +173,11 @@ impl GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
if self.contexts.lock().unwrap().is_empty() {
|
if !self.contexts.lock().unwrap().is_empty() {
|
||||||
self.generate_contexts()?;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.context_map
|
self.generate_contexts()?;
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.extend(self.stored_manifest.get_contexts());
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,19 +189,20 @@ impl GameDownloadAgent {
|
|||||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||||
create_dir_all(base_path).unwrap();
|
create_dir_all(base_path).unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||||
|
completed_contexts_lock.clear();
|
||||||
|
completed_contexts_lock
|
||||||
|
.extend_from_slice(&self.stored_manifest.get_completed_contexts());
|
||||||
|
}
|
||||||
|
|
||||||
for (raw_path, chunk) in manifest {
|
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.parent().unwrap();
|
||||||
create_dir_all(container).unwrap();
|
create_dir_all(container).unwrap();
|
||||||
|
|
||||||
let file = OpenOptions::new()
|
let file = File::create(path.clone()).unwrap();
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.create(true)
|
|
||||||
.open(path.clone())
|
|
||||||
.unwrap();
|
|
||||||
let mut running_offset = 0;
|
let mut running_offset = 0;
|
||||||
|
|
||||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||||
@ -249,156 +225,133 @@ impl GameDownloadAgent {
|
|||||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let existing_contexts = self.stored_manifest.get_completed_contexts();
|
|
||||||
self.stored_manifest.set_contexts(
|
|
||||||
&contexts
|
|
||||||
.iter()
|
|
||||||
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
|
|
||||||
.collect::<Vec<(String, bool)>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
*self.contexts.lock().unwrap() = contexts;
|
*self.contexts.lock().unwrap() = contexts;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&self) -> Result<bool, ()> {
|
// TODO: Change return value on Err
|
||||||
let max_download_threads = borrow_db_checked().await.settings.max_download_threads;
|
pub fn run(&self) -> Result<bool, ()> {
|
||||||
|
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"downloading game: {} with {} threads",
|
"downloading game: {} with {} threads",
|
||||||
self.id, max_download_threads
|
self.id, max_download_threads
|
||||||
);
|
);
|
||||||
|
let pool = ThreadPoolBuilder::new()
|
||||||
let base_url = DB
|
.num_threads(max_download_threads)
|
||||||
.fetch_base_url()
|
|
||||||
.await
|
|
||||||
.join("/api/v1/client/chunk")
|
|
||||||
.unwrap();
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let client_ref = unsafe { extend_lifetime(&client) };
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.worker_threads(max_download_threads)
|
|
||||||
.thread_name("drop-download-thread")
|
|
||||||
.enable_io()
|
|
||||||
.enable_time()
|
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| {
|
.unwrap();
|
||||||
warn!("failed to create download scheduler: {e}");
|
|
||||||
()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let (tx, mut rx) = mpsc::channel(32);
|
let completed_indexes = Arc::new(boxcar::Vec::new());
|
||||||
|
let completed_indexes_loop_arc = completed_indexes.clone();
|
||||||
|
|
||||||
// Scope this for safety
|
|
||||||
{
|
|
||||||
let contexts = self.contexts.lock().unwrap();
|
let contexts = self.contexts.lock().unwrap();
|
||||||
debug!("{contexts:#?}");
|
debug!("{:#?}", contexts);
|
||||||
|
pool.scope(|scope| {
|
||||||
let context_map = self.context_map.lock().unwrap();
|
let client = &reqwest::blocking::Client::new();
|
||||||
for (index, context) in contexts.iter().enumerate() {
|
for (index, context) in contexts.iter().enumerate() {
|
||||||
|
let client = client.clone();
|
||||||
|
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||||
|
|
||||||
let progress = self.progress.get(index);
|
let progress = self.progress.get(index);
|
||||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||||
|
|
||||||
// If we've done this one already, skip it
|
// If we've done this one already, skip it
|
||||||
if Some(&true) == context_map.get(&context.checksum) {
|
if self.completed_contexts.lock().unwrap().contains(&index) {
|
||||||
progress_handle.skip(context.length);
|
progress_handle.skip(context.length);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
let local_tx = tx.clone();
|
let request = match make_request(
|
||||||
/*
|
&client,
|
||||||
This lifetime extensions are necessary, because this loop acts like a scope
|
&["/api/v1/client/chunk"],
|
||||||
but Rust doesn't know that.
|
&[
|
||||||
*/
|
|
||||||
let context = unsafe { extend_lifetime(context) };
|
|
||||||
let self_static = unsafe { extend_lifetime(self) };
|
|
||||||
let mut base_url = base_url.clone();
|
|
||||||
rt.spawn(async move {
|
|
||||||
{
|
|
||||||
let mut query = base_url.query_pairs_mut();
|
|
||||||
let query_params = [
|
|
||||||
("id", &context.game_id),
|
("id", &context.game_id),
|
||||||
("version", &context.version),
|
("version", &context.version),
|
||||||
("name", &context.file_name),
|
("name", &context.file_name),
|
||||||
("chunk", &context.index.to_string()),
|
("chunk", &context.index.to_string()),
|
||||||
];
|
],
|
||||||
for (param, val) in query_params {
|
|r| r.header("Authorization", generate_authorization_header()),
|
||||||
query.append_pair(param.as_ref(), val.as_ref());
|
) {
|
||||||
}
|
Ok(request) => request,
|
||||||
}
|
|
||||||
|
|
||||||
let request = client_ref.get(base_url);
|
|
||||||
|
|
||||||
match download_game_chunk(
|
|
||||||
context,
|
|
||||||
&self_static.control_flag,
|
|
||||||
progress_handle,
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(true) => {
|
|
||||||
local_tx.send(context.checksum.clone()).await.unwrap();
|
|
||||||
}
|
|
||||||
Ok(false) => {}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
sender
|
||||||
|
.send(DownloadManagerSignal::Error(
|
||||||
|
ApplicationDownloadError::Communication(e),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.spawn(move |_| {
|
||||||
|
match download_game_chunk(context, &self.control_flag, progress_handle, request)
|
||||||
|
{
|
||||||
|
Ok(res) => {
|
||||||
|
if res {
|
||||||
|
completed_indexes.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let newly_completed = completed_indexes.to_owned();
|
||||||
|
|
||||||
|
let completed_lock_len = {
|
||||||
|
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
|
||||||
|
for (_, item) in newly_completed.iter() {
|
||||||
|
completed_contexts_lock.push_front(*item);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut newly_completed = Vec::new();
|
completed_contexts_lock.len()
|
||||||
while let Some(completed_checksum) = rx.recv().await {
|
};
|
||||||
newly_completed.push(completed_checksum);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'return' from the download
|
// If we're not out of contexts, we're not done, so we don't fire completed
|
||||||
let mut context_map_lock = self.context_map.lock().unwrap();
|
if completed_lock_len != contexts.len() {
|
||||||
for item in newly_completed.iter() {
|
|
||||||
context_map_lock.insert(item.clone(), true);
|
|
||||||
}
|
|
||||||
let completed_lock_len = context_map_lock.values().filter(|x| **x).count();
|
|
||||||
|
|
||||||
let contexts = self.contexts.lock().unwrap();
|
|
||||||
let contexts = contexts
|
|
||||||
.iter()
|
|
||||||
.map(|x| {
|
|
||||||
(
|
|
||||||
x.checksum.clone(),
|
|
||||||
context_map_lock.get(&x.checksum).cloned().unwrap_or(false),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<(String, bool)>>();
|
|
||||||
|
|
||||||
drop(context_map_lock);
|
|
||||||
|
|
||||||
self.stored_manifest.set_contexts(&contexts);
|
|
||||||
self.stored_manifest.write();
|
|
||||||
|
|
||||||
// If there are any contexts left which are false
|
|
||||||
if !contexts.iter().all(|x| x.1) {
|
|
||||||
info!(
|
info!(
|
||||||
"download agent for {} exited without completing ({}/{})",
|
"download agent for {} exited without completing ({}/{})",
|
||||||
self.id.clone(),
|
self.id.clone(),
|
||||||
completed_lock_len,
|
completed_lock_len,
|
||||||
contexts.len(),
|
contexts.len(),
|
||||||
);
|
);
|
||||||
|
self.stored_manifest
|
||||||
|
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
|
||||||
|
self.stored_manifest.write();
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We've completed
|
||||||
|
self.sender
|
||||||
|
.send(DownloadManagerSignal::Completed(self.metadata()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl Downloadable for GameDownloadAgent {
|
impl Downloadable for GameDownloadAgent {
|
||||||
|
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
|
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||||
|
self.download(app_handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress(&self) -> Arc<ProgressObject> {
|
||||||
|
self.progress.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_flag(&self) -> DownloadThreadControl {
|
||||||
|
self.control_flag.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn metadata(&self) -> DownloadableMetadata {
|
fn metadata(&self) -> DownloadableMetadata {
|
||||||
DownloadableMetadata {
|
DownloadableMetadata {
|
||||||
id: self.id.clone(),
|
id: self.id.clone(),
|
||||||
@ -407,75 +360,53 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||||
debug!("starting download from downloadable trait");
|
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
|
||||||
self.download(app_handle).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn progress(&self) -> Arc<ProgressObject> {
|
|
||||||
self.progress.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn control_flag(&self) -> DownloadThreadControl {
|
|
||||||
self.control_flag.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||||
}
|
}
|
||||||
|
|
||||||
async 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;
|
*self.status.lock().unwrap() = DownloadStatus::Error;
|
||||||
app_handle
|
app_handle
|
||||||
.emit("download_error", error.to_string())
|
.emit("download_error", error.to_string())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
error!("error while managing download: {error}");
|
error!("error while managing download: {}", error);
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
let mut handle = DB.borrow_data_mut().unwrap();
|
||||||
handle
|
handle
|
||||||
.applications
|
.applications
|
||||||
.transient_statuses
|
.transient_statuses
|
||||||
.remove(&self.metadata());
|
.remove(&self.metadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||||
on_game_complete(
|
on_game_complete(
|
||||||
&self.metadata(),
|
&self.metadata(),
|
||||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
||||||
app_handle,
|
app_handle,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
|
||||||
on_game_incomplete(
|
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
|
||||||
&self.metadata(),
|
let meta = self.metadata();
|
||||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||||
app_handle,
|
app_handle
|
||||||
|
.emit(
|
||||||
|
&format!("update_game/{}", meta.id),
|
||||||
|
GameUpdateEvent {
|
||||||
|
game_id: meta.id.clone(),
|
||||||
|
status: (Some(GameDownloadStatus::Remote {}), None),
|
||||||
|
version: None,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
|
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
|
||||||
|
|
||||||
async fn status(&self) -> DownloadStatus {
|
fn status(&self) -> DownloadStatus {
|
||||||
self.status.lock().unwrap().clone()
|
self.status.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn validate(&self) -> Result<bool, ApplicationDownloadError> {
|
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Validating;
|
|
||||||
let contexts = self.contexts.lock().unwrap().clone();
|
|
||||||
game_validate_logic(
|
|
||||||
&self.stored_manifest,
|
|
||||||
contexts,
|
|
||||||
self.progress.clone(),
|
|
||||||
self.sender.clone(),
|
|
||||||
&self.control_flag,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,76 +1,73 @@
|
|||||||
use crate::download_manager::util::download_thread_control_flag::{
|
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||||
DownloadThreadControl, DownloadThreadControlFlag,
|
|
||||||
};
|
|
||||||
use crate::download_manager::util::progress_object::ProgressHandle;
|
use crate::download_manager::util::progress_object::ProgressHandle;
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::drop_server_error::DropServerError;
|
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::DropDownloadContext;
|
use crate::games::downloads::manifest::DropDownloadContext;
|
||||||
use crate::remote::auth::{generate_authorization_header, generate_authorization_header_part};
|
use log::warn;
|
||||||
use futures::TryStreamExt;
|
|
||||||
use log::{debug, info, warn};
|
|
||||||
use md5::{Context, Digest};
|
use md5::{Context, Digest};
|
||||||
use reqwest::RequestBuilder;
|
use reqwest::blocking::{RequestBuilder, Response};
|
||||||
use tokio::fs::{File, OpenOptions};
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
|
||||||
use tokio_util::io::StreamReader;
|
|
||||||
|
|
||||||
use std::fs::{Permissions, set_permissions};
|
use std::fs::{set_permissions, Permissions};
|
||||||
use std::io::Write;
|
use std::io::{ErrorKind, Read};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, SeekFrom},
|
fs::{File, OpenOptions},
|
||||||
|
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DropWriter<W: AsyncWrite> {
|
pub struct DropWriter<W: Write> {
|
||||||
hasher: Context,
|
hasher: Context,
|
||||||
destination: W,
|
destination: W,
|
||||||
}
|
}
|
||||||
impl DropWriter<File> {
|
impl DropWriter<File> {
|
||||||
async fn new(path: PathBuf) -> Self {
|
fn new(path: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
destination: OpenOptions::new().write(true).open(path).await.unwrap(),
|
destination: OpenOptions::new().write(true).open(path).unwrap(),
|
||||||
hasher: Context::new(),
|
hasher: Context::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish(mut self) -> io::Result<Digest> {
|
fn finish(mut self) -> io::Result<Digest> {
|
||||||
self.flush().await.unwrap();
|
self.flush().unwrap();
|
||||||
Ok(self.hasher.compute())
|
Ok(self.hasher.compute())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async fn write(&mut self, mut buf: &[u8]) -> io::Result<()> {
|
// Write automatically pushes to file and hasher
|
||||||
self.hasher
|
impl Write for DropWriter<File> {
|
||||||
.write_all(buf)
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
.map_err(|e| io::Error::other(format!("Unable to write to hasher: {e}")))?;
|
self.hasher.write_all(buf).map_err(|e| {
|
||||||
self.destination.write_all_buf(&mut buf).await
|
io::Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
format!("Unable to write to hasher: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
self.destination.write(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn flush(&mut self) -> io::Result<()> {
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
self.hasher.flush()?;
|
self.hasher.flush()?;
|
||||||
self.destination.flush().await
|
self.destination.flush()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
// Seek moves around destination output
|
||||||
self.destination.seek(pos).await
|
impl Seek for DropWriter<File> {
|
||||||
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
|
self.destination.seek(pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DropDownloadPipeline<'a, R: AsyncRead, W: AsyncWrite> {
|
pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
||||||
pub source: R,
|
pub source: R,
|
||||||
pub destination: DropWriter<W>,
|
pub destination: DropWriter<W>,
|
||||||
pub control_flag: &'a DownloadThreadControl,
|
pub control_flag: &'a DownloadThreadControl,
|
||||||
pub progress: ProgressHandle,
|
pub progress: ProgressHandle,
|
||||||
pub size: usize,
|
pub size: usize,
|
||||||
}
|
}
|
||||||
impl<'a, R> DropDownloadPipeline<'a, R, File>
|
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||||
where
|
|
||||||
R: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
fn new(
|
fn new(
|
||||||
source: R,
|
source: Response,
|
||||||
destination: DropWriter<File>,
|
destination: DropWriter<File>,
|
||||||
control_flag: &'a DownloadThreadControl,
|
control_flag: &'a DownloadThreadControl,
|
||||||
progress: ProgressHandle,
|
progress: ProgressHandle,
|
||||||
@ -85,51 +82,38 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn copy(&mut self) -> Result<bool, io::Error> {
|
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||||
let copy_buf_size = 512;
|
let copy_buf_size = 512;
|
||||||
let mut copy_buf = vec![0; copy_buf_size];
|
let mut copy_buf = vec![0; copy_buf_size];
|
||||||
|
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||||
|
|
||||||
let mut current_size = 0;
|
let mut current_size = 0;
|
||||||
loop {
|
loop {
|
||||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||||
self.destination.flush().await?;
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut bytes_read = self.source.read(&mut copy_buf).await?;
|
let bytes_read = self.source.read(&mut copy_buf)?;
|
||||||
info!("read {}", bytes_read);
|
|
||||||
current_size += bytes_read;
|
current_size += bytes_read;
|
||||||
|
|
||||||
if current_size > self.size {
|
buf_writer.write_all(©_buf[0..bytes_read])?;
|
||||||
let over = current_size - self.size;
|
|
||||||
warn!("server sent too many bytes... {over} over");
|
|
||||||
bytes_read -= over;
|
|
||||||
current_size = self.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.destination.write(©_buf[0..bytes_read]).await?;
|
|
||||||
self.progress.add(bytes_read);
|
self.progress.add(bytes_read);
|
||||||
|
|
||||||
if current_size >= self.size {
|
if current_size == self.size {
|
||||||
debug!(
|
|
||||||
"finished with final size of {} vs {}",
|
|
||||||
current_size, self.size
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.destination.flush().await?;
|
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish(self) -> Result<Digest, io::Error> {
|
fn finish(self) -> Result<Digest, io::Error> {
|
||||||
let checksum = self.destination.finish().await?;
|
let checksum = self.destination.finish()?;
|
||||||
Ok(checksum)
|
Ok(checksum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_game_chunk(
|
pub fn download_game_chunk(
|
||||||
ctx: &DropDownloadContext,
|
ctx: &DropDownloadContext,
|
||||||
control_flag: &DownloadThreadControl,
|
control_flag: &DownloadThreadControl,
|
||||||
progress: ProgressHandle,
|
progress: ProgressHandle,
|
||||||
@ -141,33 +125,22 @@ pub async fn download_game_chunk(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let header_generator = generate_authorization_header_part().await;
|
|
||||||
|
|
||||||
let response = request
|
let response = request
|
||||||
.header("Authorization", header_generator())
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
|
||||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
debug!("chunk request got status code: {}", response.status());
|
let err = response.json().unwrap();
|
||||||
let raw_res = response.text().await.unwrap();
|
|
||||||
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
|
|
||||||
return Err(ApplicationDownloadError::Communication(
|
return Err(ApplicationDownloadError::Communication(
|
||||||
RemoteAccessError::InvalidResponse(err),
|
RemoteAccessError::InvalidResponse(err),
|
||||||
));
|
));
|
||||||
};
|
|
||||||
return Err(ApplicationDownloadError::Communication(
|
|
||||||
RemoteAccessError::UnparseableResponse(raw_res),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut destination = DropWriter::new(ctx.path.clone()).await;
|
let mut destination = DropWriter::new(ctx.path.clone());
|
||||||
|
|
||||||
if ctx.offset != 0 {
|
if ctx.offset != 0 {
|
||||||
destination
|
destination
|
||||||
.seek(SeekFrom::Start(ctx.offset))
|
.seek(SeekFrom::Start(ctx.offset))
|
||||||
.await
|
|
||||||
.expect("Failed to seek to file offset");
|
.expect("Failed to seek to file offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,27 +148,20 @@ pub async fn download_game_chunk(
|
|||||||
if content_length.is_none() {
|
if content_length.is_none() {
|
||||||
warn!("recieved 0 length content from server");
|
warn!("recieved 0 length content from server");
|
||||||
return Err(ApplicationDownloadError::Communication(
|
return Err(ApplicationDownloadError::Communication(
|
||||||
RemoteAccessError::InvalidResponse(response.json().await.unwrap()),
|
RemoteAccessError::InvalidResponse(response.json().unwrap()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let length = content_length.unwrap().try_into().unwrap();
|
let mut pipeline = DropDownloadPipeline::new(
|
||||||
|
response,
|
||||||
if length != ctx.length {
|
destination,
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
control_flag,
|
||||||
}
|
progress,
|
||||||
|
content_length.unwrap().try_into().unwrap(),
|
||||||
let response_stream = StreamReader::new(
|
|
||||||
response
|
|
||||||
.bytes_stream()
|
|
||||||
.map_err(|e| std::io::Error::other(e)),
|
|
||||||
);
|
);
|
||||||
let mut pipeline =
|
|
||||||
DropDownloadPipeline::new(response_stream, destination, control_flag, progress, length);
|
|
||||||
|
|
||||||
let completed = pipeline
|
let completed = pipeline
|
||||||
.copy()
|
.copy()
|
||||||
.await
|
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||||
if !completed {
|
if !completed {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
@ -210,7 +176,6 @@ pub async fn download_game_chunk(
|
|||||||
|
|
||||||
let checksum = pipeline
|
let checksum = pipeline
|
||||||
.finish()
|
.finish()
|
||||||
.await
|
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||||
|
|
||||||
let res = hex::encode(checksum.0);
|
let res = hex::encode(checksum.0);
|
||||||
@ -218,10 +183,5 @@ pub async fn download_game_chunk(
|
|||||||
return Err(ApplicationDownloadError::Checksum);
|
return Err(ApplicationDownloadError::Checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Successfully finished download #{}, copied {} bytes",
|
|
||||||
ctx.checksum, length
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,105 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{Read, Write},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::{debug, error, info, warn};
|
|
||||||
use native_model::{Decode, Encode};
|
|
||||||
|
|
||||||
pub type DropData = v1::DropData;
|
|
||||||
|
|
||||||
static DROP_DATA_PATH: &str = ".dropdata";
|
|
||||||
|
|
||||||
pub mod v1 {
|
|
||||||
use std::{path::PathBuf, sync::Mutex};
|
|
||||||
|
|
||||||
use native_model::native_model;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
|
||||||
pub struct DropData {
|
|
||||||
pub game_id: String,
|
|
||||||
pub game_version: String,
|
|
||||||
pub contexts: Mutex<Vec<(String, bool)>>,
|
|
||||||
pub base_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DropData {
|
|
||||||
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
|
||||||
Self {
|
|
||||||
base_path,
|
|
||||||
game_id,
|
|
||||||
game_version,
|
|
||||||
contexts: Mutex::new(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DropData {
|
|
||||||
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
|
||||||
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(_) => {
|
|
||||||
debug!("Generating new dropdata for game {game_id}");
|
|
||||||
return DropData::new(game_id, game_version, base_path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut s = Vec::new();
|
|
||||||
match file.read_to_end(&mut s) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
return DropData::new(game_id, game_version, base_path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
|
|
||||||
Ok(manifest) => manifest,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("{e}");
|
|
||||||
DropData::new(game_id, game_version, base_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn write(&self) {
|
|
||||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(_) => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match file.write_all(&manifest_raw) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => error!("{e}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
|
|
||||||
*self.contexts.lock().unwrap() = completed_contexts.to_owned();
|
|
||||||
}
|
|
||||||
pub fn get_completed_contexts(&self) -> Vec<String> {
|
|
||||||
self.contexts
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|x| if x.1 { Some(x.0.clone()) } else { None })
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
pub fn get_contexts(&self) -> Vec<(String, bool)> {
|
|
||||||
info!(
|
|
||||||
"Any contexts which are complete? {}",
|
|
||||||
self.contexts.lock().unwrap().iter().any(|x| x.1)
|
|
||||||
);
|
|
||||||
self.contexts.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod download_agent;
|
pub mod download_agent;
|
||||||
mod download_logic;
|
mod download_logic;
|
||||||
mod drop_data;
|
|
||||||
mod manifest;
|
mod manifest;
|
||||||
pub mod validate;
|
mod stored_manifest;
|
||||||
|
|||||||
79
src-tauri/src/games/downloads/stored_manifest.rs
Normal file
79
src-tauri/src/games/downloads/stored_manifest.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{Read, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{error, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_binary::binary_stream::Endian;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct StoredManifest {
|
||||||
|
game_id: String,
|
||||||
|
game_version: String,
|
||||||
|
pub completed_contexts: Mutex<Vec<usize>>,
|
||||||
|
pub base_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
static DROP_DATA_PATH: &str = ".dropdata";
|
||||||
|
|
||||||
|
impl StoredManifest {
|
||||||
|
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
base_path,
|
||||||
|
game_id,
|
||||||
|
game_version,
|
||||||
|
completed_contexts: Mutex::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||||
|
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return StoredManifest::new(game_id, game_version, base_path),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut s = Vec::new();
|
||||||
|
match file.read_to_end(&mut s) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
return StoredManifest::new(game_id, game_version, base_path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
|
||||||
|
Ok(manifest) => manifest,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{}", e);
|
||||||
|
StoredManifest::new(game_id, game_version, base_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn write(&self) {
|
||||||
|
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match file.write_all(&manifest_raw) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => error!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
|
||||||
|
*self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
|
||||||
|
}
|
||||||
|
pub fn get_completed_contexts(&self) -> Vec<usize> {
|
||||||
|
self.completed_contexts.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,172 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
|
||||||
sync::{Arc, mpsc::Sender},
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::{debug, error, info};
|
|
||||||
use md5::Context;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::db::borrow_db_checked,
|
|
||||||
download_manager::{
|
|
||||||
download_manager_frontend::DownloadManagerSignal,
|
|
||||||
util::{
|
|
||||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
|
||||||
progress_object::{ProgressHandle, ProgressObject},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
|
||||||
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn game_validate_logic(
|
|
||||||
dropdata: &DropData,
|
|
||||||
contexts: Vec<DropDownloadContext>,
|
|
||||||
progress: Arc<ProgressObject>,
|
|
||||||
sender: Sender<DownloadManagerSignal>,
|
|
||||||
control_flag: &DownloadThreadControl,
|
|
||||||
) -> Result<bool, ApplicationDownloadError> {
|
|
||||||
progress.reset(contexts.len());
|
|
||||||
let max_download_threads = borrow_db_checked().await.settings.max_download_threads;
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"validating game: {} with {} threads",
|
|
||||||
dropdata.game_id, max_download_threads
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!("{contexts:#?}");
|
|
||||||
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
|
||||||
unsafe {
|
|
||||||
async_scoped::TokioScope::scope_and_collect(|scope| {
|
|
||||||
for (index, context) in contexts.iter().enumerate() {
|
|
||||||
let current_progress = progress.get(index);
|
|
||||||
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
|
|
||||||
let invalid_chunks_scoped = invalid_chunks.clone();
|
|
||||||
let sender = sender.clone();
|
|
||||||
|
|
||||||
scope.spawn(async move {
|
|
||||||
match validate_game_chunk(context, control_flag, progress_handle) {
|
|
||||||
Ok(true) => {
|
|
||||||
debug!(
|
|
||||||
"Finished context #{} with checksum {}",
|
|
||||||
index, context.checksum
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
debug!(
|
|
||||||
"Didn't finish context #{} with checksum {}",
|
|
||||||
index, &context.checksum
|
|
||||||
);
|
|
||||||
invalid_chunks_scoped.push(context.checksum.clone());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("{e}");
|
|
||||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).await
|
|
||||||
};
|
|
||||||
|
|
||||||
// If there are any contexts left which are false
|
|
||||||
if !invalid_chunks.is_empty() {
|
|
||||||
info!(
|
|
||||||
"validation of game id {} failed for chunks {:?}",
|
|
||||||
dropdata.game_id.clone(),
|
|
||||||
invalid_chunks
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_game_chunk(
|
|
||||||
ctx: &DropDownloadContext,
|
|
||||||
control_flag: &DownloadThreadControl,
|
|
||||||
progress: ProgressHandle,
|
|
||||||
) -> Result<bool, ApplicationDownloadError> {
|
|
||||||
debug!(
|
|
||||||
"Starting chunk validation {}, {}, {} #{}",
|
|
||||||
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
|
|
||||||
);
|
|
||||||
// If we're paused
|
|
||||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
|
||||||
progress.set(0);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut source = File::open(&ctx.path).unwrap();
|
|
||||||
|
|
||||||
if ctx.offset != 0 {
|
|
||||||
source
|
|
||||||
.seek(SeekFrom::Start(ctx.offset))
|
|
||||||
.expect("Failed to seek to file offset");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hasher = md5::Context::new();
|
|
||||||
|
|
||||||
let completed =
|
|
||||||
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap();
|
|
||||||
if !completed {
|
|
||||||
return Ok(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = hex::encode(hasher.compute().0);
|
|
||||||
if res != ctx.checksum {
|
|
||||||
println!(
|
|
||||||
"Checksum failed. Correct: {}, actual: {}",
|
|
||||||
&ctx.checksum, &res
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Successfully finished verification #{}, copied {} bytes",
|
|
||||||
ctx.checksum, ctx.length
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_copy(
|
|
||||||
source: &mut File,
|
|
||||||
dest: &mut Context,
|
|
||||||
size: usize,
|
|
||||||
control_flag: &DownloadThreadControl,
|
|
||||||
progress: ProgressHandle,
|
|
||||||
) -> Result<bool, io::Error> {
|
|
||||||
let copy_buf_size = 512;
|
|
||||||
let mut copy_buf = vec![0; copy_buf_size];
|
|
||||||
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, dest);
|
|
||||||
let mut total_bytes = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
|
||||||
buf_writer.flush()?;
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut bytes_read = source.read(&mut copy_buf)?;
|
|
||||||
total_bytes += bytes_read;
|
|
||||||
|
|
||||||
// If we read over (likely), truncate our read to
|
|
||||||
// the right size
|
|
||||||
if total_bytes > size {
|
|
||||||
let over = total_bytes - size;
|
|
||||||
bytes_read -= over;
|
|
||||||
total_bytes = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf_writer.write_all(©_buf[0..bytes_read])?;
|
|
||||||
progress.add(bytes_read);
|
|
||||||
|
|
||||||
if total_bytes >= size {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf_writer.flush()?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
@ -1,32 +1,33 @@
|
|||||||
use std::fs::remove_dir_all;
|
use std::fs::remove_dir_all;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::thread::spawn;
|
||||||
|
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::AppHandle;
|
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tokio::spawn;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||||
use crate::database::models::data::{
|
use crate::database::models::data::{
|
||||||
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||||
};
|
};
|
||||||
use crate::download_manager::download_manager_frontend::DownloadStatus;
|
use crate::download_manager::download_manager::DownloadStatus;
|
||||||
|
use crate::error::library_error::LibraryError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
use crate::remote::auth::generate_authorization_header;
|
use crate::remote::auth::generate_authorization_header;
|
||||||
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||||
use crate::remote::requests::make_request;
|
use crate::remote::requests::make_request;
|
||||||
use crate::DropFunctionState;
|
use crate::{AppState, DB};
|
||||||
use bitcode::{Decode, Encode};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct FetchGameStruct {
|
pub struct FetchGameStruct {
|
||||||
game: Game,
|
game: Game,
|
||||||
status: GameStatusWithTransient,
|
status: GameStatusWithTransient,
|
||||||
version: Option<GameVersion>,
|
version: Option<GameVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
id: String,
|
id: String,
|
||||||
@ -71,30 +72,28 @@ pub struct StatsUpdateEvent {
|
|||||||
pub time: usize,
|
pub time: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_library_logic(
|
pub fn fetch_library_logic(
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
let header = generate_authorization_header().await;
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = make_request(&client, &["/api/v1/client/user/library"], &[], async |f| {
|
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
|
||||||
f.header("Authorization", header)
|
f.header("Authorization", header)
|
||||||
})
|
})?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().unwrap();
|
||||||
warn!("{err:?}");
|
warn!("{:?}", err);
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut games: Vec<Game> = response.json().await?;
|
let mut games: Vec<Game> = response.json()?;
|
||||||
|
|
||||||
let mut handle = state.lock().await;
|
let mut handle = state.lock().unwrap();
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
|
||||||
for game in games.iter() {
|
for game in games.iter() {
|
||||||
handle.games.insert(game.id.clone(), game.clone());
|
handle.games.insert(game.id.clone(), game.clone());
|
||||||
@ -107,28 +106,28 @@ pub async fn fetch_library_logic(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add games that are installed but no longer in library
|
// Add games that are installed but no longer in library
|
||||||
for meta in db_handle.applications.installed_game_version.values() {
|
for (_, meta) in &db_handle.applications.installed_game_version {
|
||||||
if games.iter().any(|e| e.id == meta.id) {
|
if games.iter().find(|e| e.id == meta.id).is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// We should always have a cache of the object
|
// We should always have a cache of the object
|
||||||
// Pass db_handle because otherwise we get a gridlock
|
// Pass db_handle because otherwise we get a gridlock
|
||||||
let game = get_cached_object_db::<String, Game>(meta.id.clone(), &db_handle).await?;
|
let game = get_cached_object_db::<String, Game>(meta.id.clone(), &db_handle)?;
|
||||||
games.push(game);
|
games.push(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(handle);
|
drop(handle);
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
cache_object("library", &games).await?;
|
cache_object("library", &games)?;
|
||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
pub async fn fetch_library_logic_offline(
|
pub fn fetch_library_logic_offline(
|
||||||
_state: tauri::State<'_, DropFunctionState<'_>>,
|
_state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
let mut games: Vec<Game> = get_cached_object("library").await?;
|
let mut games: Vec<Game> = get_cached_object("library")?;
|
||||||
|
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
|
|
||||||
games.retain(|game| {
|
games.retain(|game| {
|
||||||
db_handle
|
db_handle
|
||||||
@ -139,13 +138,13 @@ pub async fn fetch_library_logic_offline(
|
|||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
pub async fn fetch_game_logic(
|
pub fn fetch_game_logic(
|
||||||
id: String,
|
id: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let mut state_handle = state.lock().await;
|
let mut state_handle = state.lock().unwrap();
|
||||||
|
|
||||||
let handle = borrow_db_checked().await;
|
let handle = DB.borrow_data().unwrap();
|
||||||
|
|
||||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||||
let version = match metadata_option {
|
let version = match metadata_option {
|
||||||
@ -165,7 +164,7 @@ pub async fn fetch_game_logic(
|
|||||||
|
|
||||||
let game = state_handle.games.get(&id);
|
let game = state_handle.games.get(&id);
|
||||||
if let Some(game) = game {
|
if let Some(game) = game {
|
||||||
let status = GameStatusManager::fetch_state(&id).await;
|
let status = GameStatusManager::fetch_state(&id);
|
||||||
|
|
||||||
let data = FetchGameStruct {
|
let data = FetchGameStruct {
|
||||||
game: game.clone(),
|
game: game.clone(),
|
||||||
@ -173,31 +172,29 @@ pub async fn fetch_game_logic(
|
|||||||
version,
|
version,
|
||||||
};
|
};
|
||||||
|
|
||||||
cache_object(id, game).await?;
|
cache_object(id, game)?;
|
||||||
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], async |r| {
|
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
|
||||||
r.header("Authorization", generate_authorization_header().await)
|
r.header("Authorization", generate_authorization_header())
|
||||||
})
|
})?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status() == 404 {
|
if response.status() == 404 {
|
||||||
return Err(RemoteAccessError::GameNotFound(id));
|
return Err(RemoteAccessError::GameNotFound(id));
|
||||||
}
|
}
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().unwrap();
|
||||||
warn!("{err:?}");
|
warn!("{:?}", err);
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let game: Game = response.json().await?;
|
let game: Game = response.json()?;
|
||||||
state_handle.games.insert(id.clone(), game.clone());
|
state_handle.games.insert(id.clone(), game.clone());
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
@ -206,7 +203,7 @@ pub async fn fetch_game_logic(
|
|||||||
.or_insert(GameDownloadStatus::Remote {});
|
.or_insert(GameDownloadStatus::Remote {});
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
let status = GameStatusManager::fetch_state(&id).await;
|
let status = GameStatusManager::fetch_state(&id);
|
||||||
|
|
||||||
let data = FetchGameStruct {
|
let data = FetchGameStruct {
|
||||||
game: game.clone(),
|
game: game.clone(),
|
||||||
@ -214,16 +211,16 @@ pub async fn fetch_game_logic(
|
|||||||
version,
|
version,
|
||||||
};
|
};
|
||||||
|
|
||||||
cache_object(id, &game).await?;
|
cache_object(id, &game)?;
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_game_logic_offline(
|
pub fn fetch_game_logic_offline(
|
||||||
id: String,
|
id: String,
|
||||||
_state: tauri::State<'_, DropFunctionState<'_>>,
|
_state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let handle = borrow_db_checked().await;
|
let handle = DB.borrow_data().unwrap();
|
||||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||||
let version = match metadata_option {
|
let version = match metadata_option {
|
||||||
None => None,
|
None => None,
|
||||||
@ -240,8 +237,8 @@ pub async fn fetch_game_logic_offline(
|
|||||||
};
|
};
|
||||||
drop(handle);
|
drop(handle);
|
||||||
|
|
||||||
let status = GameStatusManager::fetch_state(&id).await;
|
let status = GameStatusManager::fetch_state(&id);
|
||||||
let game = get_cached_object::<String, Game>(id).await?;
|
let game = get_cached_object::<String, Game>(id)?;
|
||||||
|
|
||||||
Ok(FetchGameStruct {
|
Ok(FetchGameStruct {
|
||||||
game,
|
game,
|
||||||
@ -250,32 +247,30 @@ pub async fn fetch_game_logic_offline(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_game_verion_options_logic(
|
pub fn fetch_game_verion_options_logic(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
let response = make_request(
|
let response = make_request(
|
||||||
&client,
|
&client,
|
||||||
&["/api/v1/client/game/versions"],
|
&["/api/v1/client/game/versions"],
|
||||||
&[("id", &game_id)],
|
&[("id", &game_id)],
|
||||||
async |r| r.header("Authorization", generate_authorization_header().await),
|
|r| r.header("Authorization", generate_authorization_header()),
|
||||||
)
|
)?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().unwrap();
|
||||||
warn!("{err:?}");
|
warn!("{:?}", err);
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Vec<GameVersion> = response.json().await?;
|
let data: Vec<GameVersion> = response.json()?;
|
||||||
|
|
||||||
let state_lock = state.lock().await;
|
let state_lock = state.lock().unwrap();
|
||||||
let process_manager_lock = state_lock.process_manager.lock().await;
|
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||||
let data: Vec<GameVersion> = data
|
let data: Vec<GameVersion> = data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
|
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
|
||||||
@ -286,9 +281,9 @@ pub async fn fetch_game_verion_options_logic(
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||||
debug!("triggered uninstall for agent");
|
debug!("triggered uninstall for agent");
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.transient_statuses
|
.transient_statuses
|
||||||
@ -318,10 +313,6 @@ pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHa
|
|||||||
version_name,
|
version_name,
|
||||||
install_dir,
|
install_dir,
|
||||||
} => Some((version_name, install_dir)),
|
} => Some((version_name, install_dir)),
|
||||||
GameDownloadStatus::PartiallyInstalled {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
} => Some((version_name, install_dir)),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
} {
|
} {
|
||||||
db_handle
|
db_handle
|
||||||
@ -333,13 +324,12 @@ pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHa
|
|||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
spawn(async move {
|
spawn(move || match remove_dir_all(install_dir) {
|
||||||
match remove_dir_all(install_dir) {
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
error!("{}", e);
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle.applications.transient_statuses.remove(&meta);
|
db_handle.applications.transient_statuses.remove(&meta);
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
@ -351,9 +341,10 @@ pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHa
|
|||||||
.entry(meta.id.clone())
|
.entry(meta.id.clone())
|
||||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
save_db();
|
||||||
|
|
||||||
debug!("uninstalled game id {}", &meta.id);
|
debug!("uninstalled game id {}", &meta.id);
|
||||||
app_handle.emit("update_library", ()).unwrap();
|
app_handle.emit("update_library", {}).unwrap();
|
||||||
|
|
||||||
push_game_update(
|
push_game_update(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
@ -362,23 +353,19 @@ pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHa
|
|||||||
(Some(GameDownloadStatus::Remote {}), None),
|
(Some(GameDownloadStatus::Remote {}), None),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
warn!("invalid previous state for uninstall, failing silently.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||||
borrow_db_checked()
|
borrow_db_checked()
|
||||||
.await
|
|
||||||
.applications
|
.applications
|
||||||
.installed_game_version
|
.installed_game_version
|
||||||
.get(game_id)
|
.get(game_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn on_game_incomplete(
|
pub fn on_game_complete(
|
||||||
meta: &DownloadableMetadata,
|
meta: &DownloadableMetadata,
|
||||||
install_dir: String,
|
install_dir: String,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
@ -388,7 +375,9 @@ pub async fn on_game_incomplete(
|
|||||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = make_request(
|
let response = make_request(
|
||||||
&client,
|
&client,
|
||||||
&["/api/v1/client/game/version"],
|
&["/api/v1/client/game/version"],
|
||||||
@ -396,79 +385,13 @@ pub async fn on_game_incomplete(
|
|||||||
("id", &meta.id),
|
("id", &meta.id),
|
||||||
("version", meta.version.as_ref().unwrap()),
|
("version", meta.version.as_ref().unwrap()),
|
||||||
],
|
],
|
||||||
async |f| f.header("Authorization", generate_authorization_header().await),
|
|f| f.header("Authorization", header),
|
||||||
)
|
)?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let game_version: GameVersion = response.json().await?;
|
let game_version: GameVersion = response.json()?;
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
let mut handle = borrow_db_mut_checked();
|
||||||
handle
|
|
||||||
.applications
|
|
||||||
.game_versions
|
|
||||||
.entry(meta.id.clone())
|
|
||||||
.or_default()
|
|
||||||
.insert(meta.version.clone().unwrap(), game_version.clone());
|
|
||||||
handle
|
|
||||||
.applications
|
|
||||||
.installed_game_version
|
|
||||||
.insert(meta.id.clone(), meta.clone());
|
|
||||||
|
|
||||||
let status = GameDownloadStatus::PartiallyInstalled {
|
|
||||||
version_name: meta.version.clone().unwrap(),
|
|
||||||
install_dir,
|
|
||||||
};
|
|
||||||
|
|
||||||
handle
|
|
||||||
.applications
|
|
||||||
.game_statuses
|
|
||||||
.insert(meta.id.clone(), status.clone());
|
|
||||||
drop(handle);
|
|
||||||
app_handle
|
|
||||||
.emit(
|
|
||||||
&format!("update_game/{}", meta.id),
|
|
||||||
GameUpdateEvent {
|
|
||||||
game_id: meta.id.clone(),
|
|
||||||
status: (Some(status), None),
|
|
||||||
version: Some(game_version),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async 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 header = generate_authorization_header().await;
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = make_request(
|
|
||||||
&client,
|
|
||||||
&["/api/v1/client/game/version"],
|
|
||||||
&[
|
|
||||||
("id", &meta.id),
|
|
||||||
("version", meta.version.as_ref().unwrap()),
|
|
||||||
],
|
|
||||||
async |f| f.header("Authorization", header),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let game_version: GameVersion = response.json().await?;
|
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
|
||||||
handle
|
handle
|
||||||
.applications
|
.applications
|
||||||
.game_versions
|
.game_versions
|
||||||
@ -481,6 +404,7 @@ pub async fn on_game_complete(
|
|||||||
.insert(meta.id.clone(), meta.clone());
|
.insert(meta.id.clone(), meta.clone());
|
||||||
|
|
||||||
drop(handle);
|
drop(handle);
|
||||||
|
save_db();
|
||||||
|
|
||||||
let status = if game_version.setup_command.is_empty() {
|
let status = if game_version.setup_command.is_empty() {
|
||||||
GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
@ -494,12 +418,13 @@ pub async fn on_game_complete(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.game_statuses
|
||||||
.insert(meta.id.clone(), status.clone());
|
.insert(meta.id.clone(), status.clone());
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
save_db();
|
||||||
app_handle
|
app_handle
|
||||||
.emit(
|
.emit(
|
||||||
&format!("update_game/{}", meta.id),
|
&format!("update_game/{}", meta.id),
|
||||||
@ -522,7 +447,7 @@ pub fn push_game_update(
|
|||||||
) {
|
) {
|
||||||
app_handle
|
app_handle
|
||||||
.emit(
|
.emit(
|
||||||
&format!("update_game/{game_id}"),
|
&format!("update_game/{}", game_id),
|
||||||
GameUpdateEvent {
|
GameUpdateEvent {
|
||||||
game_id: game_id.clone(),
|
game_id: game_id.clone(),
|
||||||
status,
|
status,
|
||||||
@ -531,3 +456,51 @@ pub fn push_game_update(
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FrontendGameOptions {
|
||||||
|
launch_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_game_configuration(
|
||||||
|
game_id: String,
|
||||||
|
options: FrontendGameOptions,
|
||||||
|
) -> Result<(), LibraryError> {
|
||||||
|
let mut handle = DB.borrow_data_mut().unwrap();
|
||||||
|
let installed_version = handle
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.get(&game_id)
|
||||||
|
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
||||||
|
|
||||||
|
let id = installed_version.id.clone();
|
||||||
|
let version = installed_version.version.clone().unwrap();
|
||||||
|
|
||||||
|
let mut existing_configuration = handle
|
||||||
|
.applications
|
||||||
|
.game_versions
|
||||||
|
.get(&id)
|
||||||
|
.unwrap()
|
||||||
|
.get(&version)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// Add more options in here
|
||||||
|
existing_configuration.launch_command_template = options.launch_string;
|
||||||
|
|
||||||
|
// Add no more options past here
|
||||||
|
|
||||||
|
handle
|
||||||
|
.applications
|
||||||
|
.game_versions
|
||||||
|
.get_mut(&id)
|
||||||
|
.unwrap()
|
||||||
|
.insert(version.to_string(), existing_configuration);
|
||||||
|
|
||||||
|
drop(handle);
|
||||||
|
save_db();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -10,8 +10,8 @@ pub type GameStatusWithTransient = (
|
|||||||
pub struct GameStatusManager {}
|
pub struct GameStatusManager {}
|
||||||
|
|
||||||
impl GameStatusManager {
|
impl GameStatusManager {
|
||||||
pub async fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||||
let db_lock = borrow_db_checked().await;
|
let db_lock = borrow_db_checked();
|
||||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||||
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
||||||
None => None,
|
None => None,
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
#![feature(fn_traits)]
|
|
||||||
#![feature(duration_constructors)]
|
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
|
||||||
#![deny(clippy::all)]
|
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod games;
|
mod games;
|
||||||
|
|
||||||
@ -12,27 +7,23 @@ mod error;
|
|||||||
mod process;
|
mod process;
|
||||||
mod remote;
|
mod remote;
|
||||||
|
|
||||||
use crate::database::db::OnceCellDatabase;
|
use crate::database::db::DatabaseImpls;
|
||||||
use crate::games::commands::update_game_configuration;
|
|
||||||
use crate::process::commands::open_process_logs;
|
|
||||||
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
|
|
||||||
use bitcode::{Decode, Encode};
|
|
||||||
use client::commands::fetch_state;
|
|
||||||
use client::{
|
use client::{
|
||||||
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
||||||
cleanup::{cleanup_and_exit, quit},
|
cleanup::{cleanup_and_exit, quit},
|
||||||
};
|
};
|
||||||
|
use client::commands::fetch_state;
|
||||||
use database::commands::{
|
use database::commands::{
|
||||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
||||||
fetch_system_data, update_settings,
|
fetch_system_data, update_settings,
|
||||||
};
|
};
|
||||||
use database::db::{DATA_ROOT_DIR, DatabaseInterface, borrow_db_checked, borrow_db_mut_checked};
|
use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, DATA_ROOT_DIR};
|
||||||
use database::models::data::GameDownloadStatus;
|
use database::models::data::GameDownloadStatus;
|
||||||
use download_manager::commands::{
|
use download_manager::commands::{
|
||||||
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
||||||
};
|
};
|
||||||
|
use download_manager::download_manager::DownloadManager;
|
||||||
use download_manager::download_manager_builder::DownloadManagerBuilder;
|
use download_manager::download_manager_builder::DownloadManagerBuilder;
|
||||||
use download_manager::download_manager_frontend::DownloadManager;
|
|
||||||
use games::collections::commands::{
|
use games::collections::commands::{
|
||||||
add_game_to_collection, create_collection, delete_collection, delete_game_in_collection,
|
add_game_to_collection, create_collection, delete_collection, delete_game_in_collection,
|
||||||
fetch_collection, fetch_collections,
|
fetch_collection, fetch_collections,
|
||||||
@ -41,13 +32,13 @@ use games::commands::{
|
|||||||
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
|
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
|
||||||
};
|
};
|
||||||
use games::downloads::commands::download_game;
|
use games::downloads::commands::download_game;
|
||||||
use games::library::Game;
|
use games::library::{update_game_configuration, Game};
|
||||||
use log::{LevelFilter, debug, info, warn};
|
use log::{debug, info, warn, LevelFilter};
|
||||||
use log4rs::Config;
|
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
use log4rs::config::{Appender, Root};
|
use log4rs::config::{Appender, Root};
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
|
use log4rs::Config;
|
||||||
use process::commands::{kill_game, launch_game};
|
use process::commands::{kill_game, launch_game};
|
||||||
use process::process_manager::ProcessManager;
|
use process::process_manager::ProcessManager;
|
||||||
use remote::auth::{self, recieve_handshake};
|
use remote::auth::{self, recieve_handshake};
|
||||||
@ -58,21 +49,19 @@ use remote::commands::{
|
|||||||
use remote::fetch_object::{fetch_object, fetch_object_offline};
|
use remote::fetch_object::{fetch_object, fetch_object_offline};
|
||||||
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::env;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::panic::PanicHookInfo;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::{
|
||||||
use std::{env, panic};
|
collections::HashMap,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
};
|
||||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
|
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
|
||||||
pub enum AppStatus {
|
pub enum AppStatus {
|
||||||
@ -85,7 +74,7 @@ pub enum AppStatus {
|
|||||||
ServerUnavailable,
|
ServerUnavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
id: String,
|
id: String,
|
||||||
@ -108,13 +97,13 @@ pub struct AppState<'a> {
|
|||||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(handle: AppHandle) -> AppState<'static> {
|
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||||
let logfile = FileAppender::builder()
|
let logfile = FileAppender::builder()
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||||
)))
|
)))
|
||||||
.append(false)
|
.append(false)
|
||||||
.build(DATA_ROOT_DIR.join("./drop.log"))
|
.build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let console = ConsoleAppender::builder()
|
let console = ConsoleAppender::builder()
|
||||||
@ -144,7 +133,7 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||||
|
|
||||||
debug!("checking if database is set up");
|
debug!("checking if database is set up");
|
||||||
let is_set_up = DB.database_is_set_up().await;
|
let is_set_up = DB.database_is_set_up();
|
||||||
if !is_set_up {
|
if !is_set_up {
|
||||||
return AppState {
|
return AppState {
|
||||||
status: AppStatus::NotConfigured,
|
status: AppStatus::NotConfigured,
|
||||||
@ -158,16 +147,15 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
debug!("database is set up");
|
debug!("database is set up");
|
||||||
|
|
||||||
// TODO: Account for possible failure
|
// TODO: Account for possible failure
|
||||||
let (app_status, user) = auth::setup().await;
|
let (app_status, user) = auth::setup();
|
||||||
|
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
let mut missing_games = Vec::new();
|
let mut missing_games = Vec::new();
|
||||||
let statuses = db_handle.applications.game_statuses.clone();
|
let statuses = db_handle.applications.game_statuses.clone();
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
for (game_id, status) in statuses.into_iter() {
|
for (game_id, status) in statuses.into_iter() {
|
||||||
match status {
|
match status {
|
||||||
GameDownloadStatus::Remote {} => {}
|
GameDownloadStatus::Remote {} => {}
|
||||||
GameDownloadStatus::PartiallyInstalled { .. } => {}
|
|
||||||
GameDownloadStatus::SetupRequired {
|
GameDownloadStatus::SetupRequired {
|
||||||
version_name: _,
|
version_name: _,
|
||||||
install_dir,
|
install_dir,
|
||||||
@ -189,9 +177,9 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("detected games missing: {missing_games:?}");
|
info!("detected games missing: {:?}", missing_games);
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
for game_id in missing_games {
|
for game_id in missing_games {
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
@ -205,8 +193,8 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
debug!("finished setup!");
|
debug!("finished setup!");
|
||||||
|
|
||||||
// Sync autostart state
|
// Sync autostart state
|
||||||
if let Err(e) = sync_autostart_on_startup(&handle).await {
|
if let Err(e) = sync_autostart_on_startup(&handle) {
|
||||||
warn!("failed to sync autostart state: {e}");
|
warn!("failed to sync autostart state: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppState {
|
AppState {
|
||||||
@ -218,34 +206,11 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static DB: OnceCellDatabase = OnceCellDatabase::new();
|
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||||
pub type DropFunctionState<'a> = Mutex<AppState<'a>>;
|
|
||||||
|
|
||||||
pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
|
|
||||||
let crash_file = DATA_ROOT_DIR.join(format!(
|
|
||||||
"crash-{}.log",
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.ok()?
|
|
||||||
.as_secs()
|
|
||||||
));
|
|
||||||
let mut file = File::create_new(crash_file).ok()?;
|
|
||||||
file.write_all(format!("Drop crashed with the following panic:\n{e}").as_bytes())
|
|
||||||
.ok()?;
|
|
||||||
drop(file);
|
|
||||||
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
panic::set_hook(Box::new(|e| {
|
|
||||||
let _ = custom_panic_handler(e);
|
|
||||||
println!("{e}");
|
|
||||||
}));
|
|
||||||
|
|
||||||
let mut builder = tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_dialog::init());
|
.plugin(tauri_plugin_dialog::init());
|
||||||
|
|
||||||
@ -294,7 +259,6 @@ pub fn run() {
|
|||||||
delete_game_in_collection,
|
delete_game_in_collection,
|
||||||
// Downloads
|
// Downloads
|
||||||
download_game,
|
download_game,
|
||||||
resume_download,
|
|
||||||
move_download_in_queue,
|
move_download_in_queue,
|
||||||
pause_downloads,
|
pause_downloads,
|
||||||
resume_downloads,
|
resume_downloads,
|
||||||
@ -305,7 +269,6 @@ pub fn run() {
|
|||||||
kill_game,
|
kill_game,
|
||||||
toggle_autostart,
|
toggle_autostart,
|
||||||
get_autostart_enabled,
|
get_autostart_enabled,
|
||||||
open_process_logs
|
|
||||||
])
|
])
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@ -314,17 +277,10 @@ pub fn run() {
|
|||||||
Some(vec!["--minimize"]),
|
Some(vec!["--minimize"]),
|
||||||
))
|
))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
let state = setup(handle);
|
||||||
tauri::async_runtime::spawn(async move {
|
debug!("initialized drop client");
|
||||||
DB.init(async { DatabaseInterface::set_up_database().await })
|
app.manage(Mutex::new(state));
|
||||||
.await;
|
|
||||||
|
|
||||||
let state = setup(app.clone()).await;
|
|
||||||
info!("initialized drop client");
|
|
||||||
if !app.manage(Mutex::new(state)) {
|
|
||||||
panic!("failed to setup drop state before Tauri does")
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
@ -332,72 +288,75 @@ pub fn run() {
|
|||||||
debug!("registered all pre-defined deep links");
|
debug!("registered all pre-defined deep links");
|
||||||
}
|
}
|
||||||
|
|
||||||
let deep_link_handle = app.clone();
|
let handle = app.handle().clone();
|
||||||
|
|
||||||
|
let _main_window = tauri::WebviewWindowBuilder::new(
|
||||||
|
&handle,
|
||||||
|
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
|
||||||
|
tauri::WebviewUrl::App("index.html".into()),
|
||||||
|
)
|
||||||
|
.title("Drop Desktop App")
|
||||||
|
.min_inner_size(1000.0, 500.0)
|
||||||
|
.inner_size(1536.0, 864.0)
|
||||||
|
.decorations(false)
|
||||||
|
.shadow(false)
|
||||||
|
.data_directory(DATA_ROOT_DIR.lock().unwrap().join(".webview"))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
app.deep_link().on_open_url(move |event| {
|
app.deep_link().on_open_url(move |event| {
|
||||||
let deep_link_handle = deep_link_handle.clone();
|
|
||||||
|
|
||||||
tauri::async_runtime::block_on(async move {
|
|
||||||
debug!("handling drop:// url");
|
debug!("handling drop:// url");
|
||||||
let binding = event.urls();
|
let binding = event.urls();
|
||||||
let url = binding.first().unwrap();
|
let url = binding.first().unwrap();
|
||||||
if url.host_str().unwrap() == "handshake" {
|
if url.host_str().unwrap() == "handshake" {
|
||||||
recieve_handshake(deep_link_handle, url.path().to_string()).await
|
recieve_handshake(handle.clone(), url.path().to_string())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let menu = Menu::with_items(
|
let menu = Menu::with_items(
|
||||||
&app,
|
app,
|
||||||
&[
|
&[
|
||||||
&MenuItem::with_id(&app, "open", "Open", true, None::<&str>).unwrap(),
|
&MenuItem::with_id(app, "open", "Open", true, None::<&str>)?,
|
||||||
&PredefinedMenuItem::separator(&app).unwrap(),
|
&PredefinedMenuItem::separator(app)?,
|
||||||
/*
|
/*
|
||||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
*/
|
*/
|
||||||
&MenuItem::with_id(&app, "quit", "Quit", true, None::<&str>).unwrap(),
|
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?,
|
||||||
],
|
],
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let tray_app_handle = app.clone();
|
run_on_tray(|| {
|
||||||
run_on_tray(move || {
|
|
||||||
TrayIconBuilder::new()
|
TrayIconBuilder::new()
|
||||||
.icon(tray_app_handle.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.on_menu_event(|app, event| {
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
let tray_app_handle = app.clone();
|
|
||||||
tauri::async_runtime::block_on(async move {
|
|
||||||
match event.id.as_ref() {
|
|
||||||
"open" => {
|
"open" => {
|
||||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
cleanup_and_exit(
|
cleanup_and_exit(app, &app.state());
|
||||||
&tray_app_handle,
|
|
||||||
&tray_app_handle.state(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
warn!("menu event not handled: {:?}", event.id);
|
warn!("menu event not handled: {:?}", event.id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
.build(app)
|
||||||
.build(&tray_app_handle)
|
|
||||||
.expect("error while setting up tray menu");
|
.expect("error while setting up tray menu");
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
if let Some(original) = db_handle.prev_database.take() {
|
if let Some(original) = db_handle.prev_database.take() {
|
||||||
warn!(
|
warn!(
|
||||||
"Database corrupted. Original file at {}",
|
"Database corrupted. Original file at {}",
|
||||||
original.canonicalize().unwrap().to_string_lossy()
|
original
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
);
|
);
|
||||||
app.dialog()
|
app.dialog()
|
||||||
.message(
|
.message(
|
||||||
@ -407,49 +366,29 @@ pub fn run() {
|
|||||||
.title("Database corrupted")
|
.title("Database corrupted")
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let _main_window = tauri::WebviewWindowBuilder::new(
|
|
||||||
&app,
|
|
||||||
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
|
|
||||||
tauri::WebviewUrl::App("index.html".into()),
|
|
||||||
)
|
|
||||||
.title("Drop Desktop App")
|
|
||||||
.min_inner_size(1000.0, 500.0)
|
|
||||||
.inner_size(1536.0, 864.0)
|
|
||||||
.decorations(false)
|
|
||||||
.shadow(false)
|
|
||||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("object", move |ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("object", move |ctx, request, responder| {
|
||||||
tauri::async_runtime::block_on(async move {
|
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
|
||||||
let state = ctx.app_handle().state::<DropFunctionState<'_>>();
|
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
fetch_object,
|
fetch_object,
|
||||||
fetch_object_offline,
|
fetch_object_offline,
|
||||||
request,
|
request,
|
||||||
responder
|
responder
|
||||||
)
|
);
|
||||||
.await;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| {
|
||||||
tauri::async_runtime::block_on(async move {
|
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
|
||||||
let state = ctx.app_handle().state::<DropFunctionState<'_>>();
|
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
handle_server_proto,
|
handle_server_proto,
|
||||||
handle_server_proto_offline,
|
handle_server_proto_offline,
|
||||||
request,
|
request,
|
||||||
responder
|
responder
|
||||||
)
|
);
|
||||||
.await;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
@ -473,7 +412,7 @@ pub fn run() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_on_tray<T: FnOnce()>(f: T) {
|
fn run_on_tray<T: FnOnce() -> ()>(f: T) {
|
||||||
if match std::env::var("NO_TRAY_ICON") {
|
if match std::env::var("NO_TRAY_ICON") {
|
||||||
Ok(s) => s.to_lowercase() != "true",
|
Ok(s) => s.to_lowercase() != "true",
|
||||||
Err(_) => true,
|
Err(_) => true,
|
||||||
|
|||||||
@ -2,11 +2,5 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let global_runtime = tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.worker_threads(32)
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
tauri::async_runtime::set(global_runtime.handle().clone());
|
|
||||||
drop_app_lib::run()
|
drop_app_lib::run()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
use crate::{error::process_error::ProcessError, DropFunctionState};
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{error::process_error::ProcessError, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn launch_game(
|
pub fn launch_game(
|
||||||
id: String,
|
id: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ProcessError> {
|
) -> Result<(), ProcessError> {
|
||||||
let state_lock = state.lock().await;
|
let state_lock = state.lock().unwrap();
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().await;
|
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||||
|
|
||||||
//let meta = DownloadableMetadata {
|
//let meta = DownloadableMetadata {
|
||||||
// id,
|
// id,
|
||||||
@ -14,7 +16,7 @@ pub async fn launch_game(
|
|||||||
// download_type: DownloadType::Game,
|
// download_type: DownloadType::Game,
|
||||||
//};
|
//};
|
||||||
|
|
||||||
match process_manager_lock.launch_process(id).await {
|
match process_manager_lock.launch_process(id) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
@ -26,23 +28,13 @@ pub async fn launch_game(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn kill_game(
|
pub fn kill_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ProcessError> {
|
) -> Result<(), ProcessError> {
|
||||||
let state_lock = state.lock().await;
|
let state_lock = state.lock().unwrap();
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().await;
|
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||||
process_manager_lock
|
process_manager_lock
|
||||||
.kill_game(game_id)
|
.kill_game(game_id)
|
||||||
.map_err(ProcessError::IOError)
|
.map_err(ProcessError::IOError)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn open_process_logs(
|
|
||||||
game_id: String,
|
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
|
||||||
) -> Result<(), ProcessError> {
|
|
||||||
let state_lock = state.lock().await;
|
|
||||||
let mut process_manager_lock = state_lock.process_manager.lock().await;
|
|
||||||
process_manager_lock.open_process_logs(game_id)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::{OpenOptions, create_dir_all},
|
fs::OpenOptions,
|
||||||
io::{self},
|
io::{self},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Command, ExitStatus},
|
process::{Command, ExitStatus},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc},
|
sync::{Arc, Mutex},
|
||||||
time::{Duration, SystemTime},
|
thread::spawn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use dynfmt::Format;
|
use dynfmt::Format;
|
||||||
@ -14,9 +14,7 @@ use dynfmt::SimpleCurlyFormat;
|
|||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use shared_child::SharedChild;
|
use shared_child::SharedChild;
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
|
||||||
use tokio::spawn;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{
|
database::{
|
||||||
@ -25,26 +23,25 @@ use crate::{
|
|||||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
},
|
},
|
||||||
}, error::process_error::ProcessError, games::{library::push_game_update, state::GameStatusManager}, DropFunctionState, DB
|
},
|
||||||
|
error::process_error::ProcessError,
|
||||||
|
games::{library::push_game_update, state::GameStatusManager},
|
||||||
|
AppState, DB,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct RunningProcess {
|
|
||||||
handle: Arc<SharedChild>,
|
|
||||||
start: SystemTime,
|
|
||||||
manually_killed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ProcessManager<'a> {
|
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, Arc<SharedChild>>,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessManager<'_> {
|
impl ProcessManager<'_> {
|
||||||
pub fn new(app_handle: AppHandle) -> Self {
|
pub fn new(app_handle: AppHandle) -> Self {
|
||||||
let log_output_dir = DATA_ROOT_DIR.join("logs");
|
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
|
||||||
|
let log_output_dir = root_dir_lock.join("logs");
|
||||||
|
drop(root_dir_lock);
|
||||||
|
|
||||||
ProcessManager {
|
ProcessManager {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@ -82,11 +79,10 @@ impl ProcessManager<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
|
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
|
||||||
match self.processes.get_mut(&game_id) {
|
match self.processes.get(&game_id) {
|
||||||
Some(process) => {
|
Some(child) => {
|
||||||
process.manually_killed = true;
|
child.kill()?;
|
||||||
process.handle.kill()?;
|
child.wait()?;
|
||||||
process.handle.wait()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
None => Err(io::Error::new(
|
None => Err(io::Error::new(
|
||||||
@ -96,28 +92,17 @@ impl ProcessManager<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> {
|
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
|
||||||
let dir = self.log_output_dir.join(game_id);
|
|
||||||
self.app_handle
|
|
||||||
.opener()
|
|
||||||
.open_path(dir.to_str().unwrap(), None::<&str>)
|
|
||||||
.map_err(ProcessError::OpenerError)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("process for {:?} exited with {:?}", &game_id, result);
|
debug!("process for {:?} exited with {:?}", &game_id, result);
|
||||||
|
|
||||||
let process = self.processes.remove(&game_id).unwrap();
|
self.processes.remove(&game_id);
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked().await;
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
let meta = db_handle
|
let meta = db_handle
|
||||||
.applications
|
.applications
|
||||||
.installed_game_version
|
.installed_game_version
|
||||||
@ -127,13 +112,14 @@ impl ProcessManager<'_> {
|
|||||||
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();
|
||||||
if let Some(GameDownloadStatus::SetupRequired {
|
if let Some(saved_state) = current_state {
|
||||||
|
if let GameDownloadStatus::SetupRequired {
|
||||||
version_name,
|
version_name,
|
||||||
install_dir,
|
install_dir,
|
||||||
}) = current_state
|
} = saved_state
|
||||||
&& let Ok(exit_code) = result
|
|
||||||
&& exit_code.success()
|
|
||||||
{
|
{
|
||||||
|
if let Ok(exit_code) = result {
|
||||||
|
if exit_code.success() {
|
||||||
db_handle.applications.game_statuses.insert(
|
db_handle.applications.game_statuses.insert(
|
||||||
game_id.clone(),
|
game_id.clone(),
|
||||||
GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
@ -142,36 +128,33 @@ impl ProcessManager<'_> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
|
let status = GameStatusManager::fetch_state(&game_id);
|
||||||
// If we started and ended really quickly, something might've gone wrong
|
|
||||||
// Or if the status isn't 0
|
|
||||||
// Or if it's an error
|
|
||||||
if !process.manually_killed
|
|
||||||
&& (elapsed.as_secs() <= 2 || result.is_err() || !result.unwrap().success())
|
|
||||||
{
|
|
||||||
warn!("drop detected that the game {game_id} may have failed to launch properly");
|
|
||||||
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = GameStatusManager::fetch_state(&game_id).await;
|
|
||||||
push_game_update(&self.app_handle, &game_id, None, status);
|
push_game_update(&self.app_handle, &game_id, None, status);
|
||||||
|
|
||||||
|
// TODO better management
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
|
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
|
||||||
let current = &self.current_platform;
|
let current = &self.current_platform;
|
||||||
Ok(self.game_launchers.contains_key(&(*current, *platform)))
|
Ok(self
|
||||||
|
.game_launchers
|
||||||
|
.contains_key(&(current.clone(), platform.clone())))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
|
pub fn launch_process(&mut self, game_id: String) -> 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 DB
|
||||||
.borrow_data()
|
.borrow_data()
|
||||||
.await
|
.unwrap()
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.game_statuses
|
||||||
.get(&game_id)
|
.get(&game_id)
|
||||||
@ -179,7 +162,7 @@ impl ProcessManager<'_> {
|
|||||||
{
|
{
|
||||||
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
|
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
|
||||||
Some(GameDownloadStatus::SetupRequired { .. }) => {
|
Some(GameDownloadStatus::SetupRequired { .. }) => {
|
||||||
return Err(ProcessError::SetupRequired);
|
return Err(ProcessError::SetupRequired)
|
||||||
}
|
}
|
||||||
_ => return Err(ProcessError::NotInstalled),
|
_ => return Err(ProcessError::NotInstalled),
|
||||||
};
|
};
|
||||||
@ -189,7 +172,7 @@ impl ProcessManager<'_> {
|
|||||||
download_type: DownloadType::Game,
|
download_type: DownloadType::Game,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut db_lock = borrow_db_mut_checked().await;
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
debug!(
|
debug!(
|
||||||
"Launching process {:?} with games {:?}",
|
"Launching process {:?} with games {:?}",
|
||||||
&game_id, db_lock.applications.game_versions
|
&game_id, db_lock.applications.game_versions
|
||||||
@ -210,10 +193,6 @@ impl ProcessManager<'_> {
|
|||||||
version_name,
|
version_name,
|
||||||
install_dir,
|
install_dir,
|
||||||
} => (version_name, install_dir),
|
} => (version_name, install_dir),
|
||||||
GameDownloadStatus::PartiallyInstalled {
|
|
||||||
version_name,
|
|
||||||
install_dir,
|
|
||||||
} => (version_name, install_dir),
|
|
||||||
_ => return Err(ProcessError::NotDownloaded),
|
_ => return Err(ProcessError::NotDownloaded),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -225,17 +204,18 @@ impl ProcessManager<'_> {
|
|||||||
.get(version_name)
|
.get(version_name)
|
||||||
.ok_or(ProcessError::InvalidVersion)?;
|
.ok_or(ProcessError::InvalidVersion)?;
|
||||||
|
|
||||||
// TODO: refactor this path with open_process_logs
|
|
||||||
let game_log_folder = &self.log_output_dir.join(game_id);
|
|
||||||
create_dir_all(game_log_folder).map_err(ProcessError::IOError)?;
|
|
||||||
|
|
||||||
let current_time = chrono::offset::Local::now();
|
let current_time = chrono::offset::Local::now();
|
||||||
let log_file = OpenOptions::new()
|
let log_file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.read(true)
|
.read(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.open(game_log_folder.join(format!("{}-{}.log", &version, current_time.timestamp())))
|
.open(self.log_output_dir.join(format!(
|
||||||
|
"{}-{}-{}.log",
|
||||||
|
&game_id,
|
||||||
|
&version,
|
||||||
|
current_time.timestamp()
|
||||||
|
)))
|
||||||
.map_err(ProcessError::IOError)?;
|
.map_err(ProcessError::IOError)?;
|
||||||
|
|
||||||
let error_file = OpenOptions::new()
|
let error_file = OpenOptions::new()
|
||||||
@ -243,15 +223,16 @@ impl ProcessManager<'_> {
|
|||||||
.truncate(true)
|
.truncate(true)
|
||||||
.read(true)
|
.read(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.open(game_log_folder.join(format!(
|
.open(self.log_output_dir.join(format!(
|
||||||
"{}-{}-error.log",
|
"{}-{}-{}-error.log",
|
||||||
|
&game_id,
|
||||||
&version,
|
&version,
|
||||||
current_time.timestamp()
|
current_time.timestamp()
|
||||||
)))
|
)))
|
||||||
.map_err(ProcessError::IOError)?;
|
.map_err(ProcessError::IOError)?;
|
||||||
|
|
||||||
let current_platform = self.current_platform;
|
let current_platform = self.current_platform.clone();
|
||||||
let target_platform = game_version.platform;
|
let target_platform = game_version.platform.clone();
|
||||||
|
|
||||||
let game_launcher = self
|
let game_launcher = self
|
||||||
.game_launchers
|
.game_launchers
|
||||||
@ -267,14 +248,10 @@ impl ProcessManager<'_> {
|
|||||||
version_name: _,
|
version_name: _,
|
||||||
install_dir: _,
|
install_dir: _,
|
||||||
} => (&game_version.setup_command, &game_version.setup_args),
|
} => (&game_version.setup_command, &game_version.setup_args),
|
||||||
GameDownloadStatus::PartiallyInstalled {
|
GameDownloadStatus::Remote {} => unreachable!("nuh uh"),
|
||||||
version_name: _,
|
|
||||||
install_dir: _,
|
|
||||||
} => unreachable!("Game registered as 'Partially Installed'"),
|
|
||||||
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
|
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);
|
||||||
let launch = launch.to_str().unwrap();
|
let launch = launch.to_str().unwrap();
|
||||||
|
|
||||||
let launch_string = game_launcher.create_launch_process(
|
let launch_string = game_launcher.create_launch_process(
|
||||||
@ -298,12 +275,12 @@ impl ProcessManager<'_> {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
command.raw_arg(format!("/C \"{}\"", &launch_string));
|
command.raw_arg(format!("/C \"{}\"", &launch_string));
|
||||||
|
|
||||||
info!("launching (in {install_dir}): {launch_string}",);
|
info!("launching (in {}): {}", install_dir, launch_string,);
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let mut command: Command = Command::new("sh");
|
let mut command: Command = Command::new("sh");
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
command.args(vec!["-c", &launch_string]);
|
command.arg("-c").arg(launch_string);
|
||||||
|
|
||||||
command
|
command
|
||||||
.stderr(error_file)
|
.stderr(error_file)
|
||||||
@ -331,14 +308,14 @@ impl ProcessManager<'_> {
|
|||||||
let wait_thread_apphandle = self.app_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(async move {
|
spawn(move || {
|
||||||
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
||||||
|
|
||||||
let app_state = wait_thread_apphandle.state::<tauri::State<'_, DropFunctionState<'_>>>();
|
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
||||||
let app_state_handle = app_state.lock().await;
|
let app_state_handle = app_state.lock().unwrap();
|
||||||
|
|
||||||
let mut process_manager_handle = app_state_handle.process_manager.lock().await;
|
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
||||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result).await;
|
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
||||||
|
|
||||||
// As everything goes out of scope, they should get dropped
|
// As everything goes out of scope, they should get dropped
|
||||||
// But just to explicit about it
|
// But just to explicit about it
|
||||||
@ -346,14 +323,7 @@ impl ProcessManager<'_> {
|
|||||||
drop(app_state_handle);
|
drop(app_state_handle);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.processes.insert(
|
self.processes.insert(meta.id, wait_thread_handle);
|
||||||
meta.id,
|
|
||||||
RunningProcess {
|
|
||||||
handle: wait_thread_handle,
|
|
||||||
start: SystemTime::now(),
|
|
||||||
manually_killed: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -366,6 +336,9 @@ pub enum Platform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Platform {
|
impl Platform {
|
||||||
|
const WINDOWS: bool = cfg!(target_os = "windows");
|
||||||
|
const MAC: bool = cfg!(target_os = "macos");
|
||||||
|
const LINUX: bool = cfg!(target_os = "linux");
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub const HOST: Platform = Self::Windows;
|
pub const HOST: Platform = Self::Windows;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@ -373,6 +346,8 @@ impl Platform {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub const HOST: Platform = Self::Linux;
|
pub const HOST: Platform = Self::Linux;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn is_case_sensitive(&self) -> bool {
|
pub fn is_case_sensitive(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Windows | Self::MacOs => false,
|
Self::Windows | Self::MacOs => false,
|
||||||
@ -398,7 +373,7 @@ impl From<whoami::Platform> for Platform {
|
|||||||
whoami::Platform::Windows => Platform::Windows,
|
whoami::Platform::Windows => Platform::Windows,
|
||||||
whoami::Platform::Linux => Platform::Linux,
|
whoami::Platform::Linux => Platform::Linux,
|
||||||
whoami::Platform::MacOS => Platform::MacOs,
|
whoami::Platform::MacOS => Platform::MacOs,
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -441,13 +416,10 @@ impl ProcessHandler for UMULauncher {
|
|||||||
) -> String {
|
) -> String {
|
||||||
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) => game_override
|
||||||
if game_override.is_empty() {
|
.is_empty()
|
||||||
game_version.game_id.clone()
|
.then_some(game_version.game_id.clone())
|
||||||
} else {
|
.unwrap_or(game_override.clone()),
|
||||||
game_override.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => game_version.game_id.clone(),
|
None => game_version.game_id.clone(),
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@ -3,18 +3,18 @@ use std::{collections::HashMap, env};
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use droplet_rs::ssl::sign_nonce;
|
use droplet_rs::ssl::sign_nonce;
|
||||||
use gethostname::gethostname;
|
use gethostname::gethostname;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppStatus, DropFunctionState, User,
|
|
||||||
database::{
|
database::{
|
||||||
db::{borrow_db_checked, borrow_db_mut_checked},
|
db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||||
models::data::DatabaseAuth,
|
models::data::DatabaseAuth,
|
||||||
},
|
},
|
||||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||||
|
AppStatus, User,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -49,39 +49,30 @@ struct HandshakeResponse {
|
|||||||
id: String,
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_authorization_header() -> String {
|
pub fn generate_authorization_header() -> String {
|
||||||
let func = generate_authorization_header_part().await;
|
|
||||||
func()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_authorization_header_part() -> Box<dyn FnOnce() -> String> {
|
|
||||||
let certs = {
|
let certs = {
|
||||||
let db = borrow_db_checked().await;
|
let db = borrow_db_checked();
|
||||||
db.auth.clone().unwrap()
|
db.auth.clone().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::new(move || {
|
|
||||||
let nonce = Utc::now().timestamp_millis().to_string();
|
let nonce = Utc::now().timestamp_millis().to_string();
|
||||||
|
|
||||||
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
||||||
|
|
||||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||||
let header = generate_authorization_header().await;
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = make_request(&client, &["/api/v1/client/user"], &[], async |f| {
|
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
|
||||||
f.header("Authorization", header)
|
f.header("Authorization", header)
|
||||||
})
|
})?
|
||||||
.await?
|
.send()?;
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err: DropServerError = response.json().await?;
|
let err: DropServerError = response.json()?;
|
||||||
warn!("{err:?}");
|
warn!("{:?}", err);
|
||||||
|
|
||||||
if err.status_message == "Nonce expired" {
|
if err.status_message == "Nonce expired" {
|
||||||
return Err(RemoteAccessError::OutOfSync);
|
return Err(RemoteAccessError::OutOfSync);
|
||||||
@ -90,10 +81,10 @@ pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
|||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
response.json::<User>().await.map_err(|e| e.into())
|
response.json::<User>().map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||||
let path_chunks: Vec<&str> = path.split("/").collect();
|
let path_chunks: Vec<&str> = path.split("/").collect();
|
||||||
if path_chunks.len() != 3 {
|
if path_chunks.len() != 3 {
|
||||||
app.emit("auth/failed", ()).unwrap();
|
app.emit("auth/failed", ()).unwrap();
|
||||||
@ -103,7 +94,7 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
let base_url = {
|
let base_url = {
|
||||||
let handle = borrow_db_checked().await;
|
let handle = borrow_db_checked();
|
||||||
Url::parse(handle.base_url.as_str())?
|
Url::parse(handle.base_url.as_str())?
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,68 +106,63 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
};
|
};
|
||||||
|
|
||||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = client.post(endpoint).json(&body).send().await?;
|
let response = client.post(endpoint).json(&body).send()?;
|
||||||
debug!("handshake responsded with {}", response.status().as_u16());
|
debug!("handshake responsded with {}", response.status().as_u16());
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
return Err(RemoteAccessError::InvalidResponse(response.json()?));
|
||||||
}
|
}
|
||||||
let response_struct: HandshakeResponse = response.json().await?;
|
let response_struct: HandshakeResponse = response.json()?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
let mut handle = borrow_db_mut_checked();
|
||||||
handle.auth = Some(DatabaseAuth {
|
handle.auth = Some(DatabaseAuth {
|
||||||
private: response_struct.private,
|
private: response_struct.private,
|
||||||
cert: response_struct.certificate,
|
cert: response_struct.certificate,
|
||||||
client_id: response_struct.id,
|
client_id: response_struct.id,
|
||||||
web_token: None, // gets created later
|
web_token: None, // gets created later
|
||||||
});
|
});
|
||||||
|
drop(handle);
|
||||||
|
save_db();
|
||||||
}
|
}
|
||||||
|
|
||||||
let web_token = {
|
let web_token = {
|
||||||
let header = generate_authorization_header().await;
|
let header = generate_authorization_header();
|
||||||
let token = client
|
let token = client
|
||||||
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
||||||
.header("Authorization", header)
|
.header("Authorization", header)
|
||||||
.send()
|
.send()
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
token.text().await.unwrap()
|
token.text().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
let mut handle = borrow_db_mut_checked();
|
||||||
let mut_auth = handle.auth.as_mut().unwrap();
|
let mut_auth = handle.auth.as_mut().unwrap();
|
||||||
mut_auth.web_token = Some(web_token);
|
mut_auth.web_token = Some(web_token);
|
||||||
|
drop(handle);
|
||||||
|
save_db();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||||
// Tell the app we're processing
|
// Tell the app we're processing
|
||||||
app.emit("auth/processing", ()).unwrap();
|
app.emit("auth/processing", ()).unwrap();
|
||||||
|
|
||||||
let handshake_result = recieve_handshake_logic(&app, path).await;
|
let handshake_result = recieve_handshake_logic(&app, path);
|
||||||
if let Err(e) = handshake_result {
|
if let Err(e) = handshake_result {
|
||||||
warn!("error with authentication: {e}");
|
warn!("error with authentication: {}", e);
|
||||||
app.emit("auth/failed", e.to_string()).unwrap();
|
app.emit("auth/failed", e.to_string()).unwrap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_state = app.state::<DropFunctionState<'_>>();
|
|
||||||
|
|
||||||
let (app_status, user) = setup().await;
|
|
||||||
|
|
||||||
let mut state_lock = app_state.lock().await;
|
|
||||||
state_lock.status = app_status;
|
|
||||||
state_lock.user = user;
|
|
||||||
|
|
||||||
app.emit("auth/finished", ()).unwrap();
|
app.emit("auth/finished", ()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||||
let base_url = {
|
let base_url = {
|
||||||
let db_lock = borrow_db_checked().await;
|
let db_lock = borrow_db_checked();
|
||||||
Url::parse(&db_lock.base_url.clone())?
|
Url::parse(&db_lock.base_url.clone())?
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -192,40 +178,40 @@ pub async fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
|||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = client.post(endpoint.to_string()).json(&body).send().await?;
|
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let data: DropServerError = response.json().await?;
|
let data: DropServerError = response.json()?;
|
||||||
error!("could not start handshake: {}", data.status_message);
|
error!("could not start handshake: {}", data.status_message);
|
||||||
|
|
||||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||||
}
|
}
|
||||||
|
|
||||||
let redir_url = response.text().await?;
|
let redir_url = response.text()?;
|
||||||
let complete_redir_url = base_url.join(&redir_url)?;
|
let complete_redir_url = base_url.join(&redir_url)?;
|
||||||
|
|
||||||
info!("opening web browser to continue authentication: {}", complete_redir_url);
|
debug!("opening web browser to continue authentication");
|
||||||
webbrowser::open(complete_redir_url.as_ref()).unwrap();
|
webbrowser::open(complete_redir_url.as_ref()).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setup() -> (AppStatus, Option<User>) {
|
pub fn setup() -> (AppStatus, Option<User>) {
|
||||||
let data = borrow_db_checked().await;
|
let data = borrow_db_checked();
|
||||||
let auth = data.auth.clone();
|
let auth = data.auth.clone();
|
||||||
drop(data);
|
drop(data);
|
||||||
|
|
||||||
if auth.is_some() {
|
if auth.is_some() {
|
||||||
let user_result = match fetch_user().await {
|
let user_result = match fetch_user() {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(RemoteAccessError::FetchError(_)) => {
|
Err(RemoteAccessError::FetchError(_)) => {
|
||||||
let user = get_cached_object::<_, User>("user").await.unwrap();
|
let user = get_cached_object::<String, User>("user".to_owned()).unwrap();
|
||||||
return (AppStatus::Offline, Some(user));
|
return (AppStatus::Offline, Some(user));
|
||||||
}
|
}
|
||||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||||
};
|
};
|
||||||
cache_object("user", &user_result).await.unwrap();
|
cache_object("user", &user_result).unwrap();
|
||||||
return (AppStatus::SignedIn, Some(user_result));
|
return (AppStatus::SignedIn, Some(user_result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,85 +1,49 @@
|
|||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{
|
database::{db::borrow_db_checked, models::data::Database},
|
||||||
db::borrow_db_checked,
|
|
||||||
models::data::Database,
|
|
||||||
},
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::remote_access_error::RemoteAccessError,
|
||||||
};
|
};
|
||||||
use bitcode::{Decode, DecodeOwned, Encode};
|
|
||||||
use cacache::Integrity;
|
use cacache::Integrity;
|
||||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
|
||||||
use log::info;
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
use serde_binary::binary_stream::Endian;
|
||||||
|
|
||||||
#[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 || if $crate::borrow_db_checked().await.settings.force_offline || $var.lock().await.status == $crate::AppStatus::Offline {
|
if crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == crate::AppStatus::Offline {
|
||||||
$func2( $( $arg ), *).await
|
$func2( $( $arg ), *)
|
||||||
} else {
|
} else {
|
||||||
$func1( $( $arg ), *).await
|
$func1( $( $arg ), *)
|
||||||
})()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cache_object<K: AsRef<str>, D: Encode>(
|
pub fn cache_object<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
|
||||||
key: K,
|
key: K,
|
||||||
data: &D,
|
data: &D,
|
||||||
) -> Result<Integrity, RemoteAccessError> {
|
) -> Result<Integrity, RemoteAccessError> {
|
||||||
let bytes = bitcode::encode(data);
|
let bytes = serde_binary::to_vec(data, Endian::Little).unwrap();
|
||||||
cacache::write_sync(&borrow_db_checked().await.cache_dir, key, bytes)
|
cacache::write_sync(&borrow_db_checked().cache_dir, key, bytes)
|
||||||
.map_err(RemoteAccessError::Cache)
|
.map_err(|e| RemoteAccessError::Cache(e))
|
||||||
}
|
}
|
||||||
pub async fn get_cached_object<K: AsRef<str> + Display, D: Encode + DecodeOwned>(
|
pub fn get_cached_object<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
|
||||||
key: K,
|
key: K,
|
||||||
) -> Result<D, RemoteAccessError> {
|
) -> Result<D, RemoteAccessError> {
|
||||||
get_cached_object_db::<K, D>(key, &&(borrow_db_checked().await)).await
|
get_cached_object_db::<K, D>(key, &borrow_db_checked())
|
||||||
}
|
}
|
||||||
pub async fn get_cached_object_db<'a, K: AsRef<str> + Display, D: DecodeOwned>(
|
pub fn get_cached_object_db<'a, K: AsRef<str>, D: Serialize + DeserializeOwned>(
|
||||||
key: K,
|
key: K,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
) -> Result<D, RemoteAccessError> {
|
) -> Result<D, RemoteAccessError> {
|
||||||
let start = SystemTime::now();
|
let bytes = cacache::read_sync(&db.cache_dir, key).map_err(|e| RemoteAccessError::Cache(e))?;
|
||||||
let bytes = cacache::read(&db.cache_dir, &key)
|
let data = serde_binary::from_slice::<D>(&bytes, Endian::Little).unwrap();
|
||||||
.await
|
|
||||||
.map_err(RemoteAccessError::Cache)?;
|
|
||||||
let read = start.elapsed().unwrap();
|
|
||||||
let data = bitcode::decode::<D>(&bytes).map_err(|_| {
|
|
||||||
RemoteAccessError::Cache(cacache::Error::EntryNotFound(
|
|
||||||
db.cache_dir.clone(),
|
|
||||||
key.to_string(),
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let parse = start.elapsed().unwrap().abs_diff(read);
|
|
||||||
info!(
|
|
||||||
"read object: r: {}, p: {}, b: {}",
|
|
||||||
read.as_millis(),
|
|
||||||
parse.as_millis(),
|
|
||||||
bytes.len()
|
|
||||||
);
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
#[derive(Encode, Decode)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ObjectCache {
|
pub struct ObjectCache {
|
||||||
content_type: String,
|
content_type: String,
|
||||||
body: Vec<u8>,
|
body: Vec<u8>,
|
||||||
expiry: u128,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectCache {
|
|
||||||
pub fn has_expired(&self) -> bool {
|
|
||||||
let duration = Duration::from_millis(self.expiry.try_into().unwrap());
|
|
||||||
SystemTime::UNIX_EPOCH
|
|
||||||
.checked_add(duration)
|
|
||||||
.unwrap()
|
|
||||||
.elapsed()
|
|
||||||
.is_err()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Response<Vec<u8>>> for ObjectCache {
|
impl From<Response<Vec<u8>>> for ObjectCache {
|
||||||
@ -93,12 +57,6 @@ impl From<Response<Vec<u8>>> for ObjectCache {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
body: value.body().clone(),
|
body: value.body().clone(),
|
||||||
expiry: SystemTime::now()
|
|
||||||
.checked_add(Duration::from_days(1))
|
|
||||||
.unwrap()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,9 +66,3 @@ impl From<ObjectCache> for Response<Vec<u8>> {
|
|||||||
resp_builder.body(value.body).unwrap()
|
resp_builder.body(value.body).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<&ObjectCache> for Response<Vec<u8>> {
|
|
||||||
fn from(value: &ObjectCache) -> Self {
|
|
||||||
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
|
|
||||||
resp_builder.body(value.body.clone()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,33 +1,35 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use reqwest::Client;
|
use reqwest::blocking::Client;
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppStatus, DropFunctionState,
|
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||||
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::remote_access_error::RemoteAccessError,
|
||||||
remote::{auth::generate_authorization_header, requests::make_request},
|
remote::{auth::generate_authorization_header, requests::make_request},
|
||||||
|
AppState, AppStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::{auth_initiate_logic, recieve_handshake, setup},
|
auth::{auth_initiate_logic, recieve_handshake, setup},
|
||||||
cache::{cache_object, get_cached_object},
|
cache::{cache_object, get_cached_object},
|
||||||
utils::use_remote_logic,
|
remote::use_remote_logic,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn use_remote(
|
pub fn use_remote(
|
||||||
url: String,
|
url: String,
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
use_remote_logic(url, state).await
|
use_remote_logic(url, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||||
let base_url = {
|
let base_url = {
|
||||||
let handle = borrow_db_checked().await;
|
let handle = borrow_db_checked();
|
||||||
|
|
||||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||||
};
|
};
|
||||||
@ -38,39 +40,39 @@ pub async fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
||||||
let _drop_url = gen_drop_url(path.clone()).await?;
|
let _drop_url = gen_drop_url(path.clone())?;
|
||||||
let req = make_request(&Client::new(), &[&path], &[], async |r| {
|
let req = make_request(&Client::new(), &[&path], &[], |r| {
|
||||||
r.header("Authorization", generate_authorization_header().await)
|
r.header("Authorization", generate_authorization_header())
|
||||||
})
|
})?
|
||||||
.await?
|
.send();
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match req {
|
match req {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let data = data.bytes().await?.to_vec();
|
let data = data.bytes()?.to_vec();
|
||||||
cache_object(&path, &data).await?;
|
cache_object(&path, &data)?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("{e}");
|
debug!("{}", e);
|
||||||
get_cached_object::<&str, Vec<u8>>(&path).await
|
get_cached_object::<&str, Vec<u8>>(&path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn sign_out(app: AppHandle) {
|
pub fn sign_out(app: AppHandle) {
|
||||||
// Clear auth from database
|
// Clear auth from database
|
||||||
{
|
{
|
||||||
let mut handle = borrow_db_mut_checked().await;
|
let mut handle = borrow_db_mut_checked();
|
||||||
handle.auth = None;
|
handle.auth = None;
|
||||||
|
drop(handle);
|
||||||
|
save_db();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update app state
|
// Update app state
|
||||||
{
|
{
|
||||||
let app_state = app.state::<DropFunctionState<'_>>();
|
let app_state = app.state::<Mutex<AppState>>();
|
||||||
let mut app_state_handle = app_state.lock().await;
|
let mut app_state_handle = app_state.lock().unwrap();
|
||||||
app_state_handle.status = AppStatus::SignedOut;
|
app_state_handle.status = AppStatus::SignedOut;
|
||||||
app_state_handle.user = None;
|
app_state_handle.user = None;
|
||||||
}
|
}
|
||||||
@ -80,23 +82,21 @@ pub async fn sign_out(app: AppHandle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn retry_connect(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
let (app_status, user) = setup().await;
|
let (app_status, user) = setup();
|
||||||
|
|
||||||
let mut guard = state.lock().await;
|
let mut guard = state.lock().unwrap();
|
||||||
guard.status = app_status;
|
guard.status = app_status;
|
||||||
guard.user = user;
|
guard.user = user;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_initiate() -> Result<(), RemoteAccessError> {
|
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
|
||||||
auth_initiate_logic().await
|
auth_initiate_logic()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn manual_recieve_handshake(app: AppHandle, token: String) {
|
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||||
recieve_handshake(app, format!("handshake/{token}")).await;
|
recieve_handshake(app, format!("handshake/{}", token));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,39 +4,28 @@ use tauri::UriSchemeResponder;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::generate_authorization_header,
|
auth::generate_authorization_header,
|
||||||
cache::{ObjectCache, cache_object, get_cached_object},
|
cache::{cache_object, get_cached_object, ObjectCache},
|
||||||
requests::make_request,
|
requests::make_request,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
// Drop leading /
|
// Drop leading /
|
||||||
let object_id = &request.uri().path()[1..];
|
let object_id = &request.uri().path()[1..];
|
||||||
|
|
||||||
let cache_result = get_cached_object::<&str, ObjectCache>(object_id).await;
|
let header = generate_authorization_header();
|
||||||
if let Ok(cache_result) = &cache_result
|
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
||||||
&& !cache_result.has_expired()
|
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
|
||||||
{
|
f.header("Authorization", header)
|
||||||
responder.respond(cache_result.into());
|
})
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let header = generate_authorization_header().await;
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = make_request(
|
|
||||||
&client,
|
|
||||||
&["/api/v1/client/object/", object_id],
|
|
||||||
&[],
|
|
||||||
async |f| f.header("Authorization", header),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.send()
|
.send();
|
||||||
.await;
|
|
||||||
if response.is_err() {
|
if response.is_err() {
|
||||||
match cache_result {
|
let data = get_cached_object::<&str, ObjectCache>(object_id);
|
||||||
Ok(cache_result) => responder.respond(cache_result.into()),
|
|
||||||
|
match data {
|
||||||
|
Ok(data) => responder.respond(data.into()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("{e}")
|
warn!("{}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -47,22 +36,18 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
|
|||||||
CONTENT_TYPE,
|
CONTENT_TYPE,
|
||||||
response.headers().get("Content-Type").unwrap(),
|
response.headers().get("Content-Type").unwrap(),
|
||||||
);
|
);
|
||||||
let data = Vec::from(response.bytes().await.unwrap());
|
let data = Vec::from(response.bytes().unwrap());
|
||||||
let resp = resp_builder.body(data).unwrap();
|
let resp = resp_builder.body(data).unwrap();
|
||||||
if cache_result.is_err() || cache_result.unwrap().has_expired() {
|
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
|
||||||
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
responder.respond(resp);
|
responder.respond(resp);
|
||||||
}
|
}
|
||||||
pub async fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
let object_id = &request.uri().path()[1..];
|
let object_id = &request.uri().path()[1..];
|
||||||
let data = get_cached_object::<&str, ObjectCache>(object_id).await;
|
let data = get_cached_object::<&str, ObjectCache>(object_id);
|
||||||
|
|
||||||
match data {
|
match data {
|
||||||
Ok(data) => responder.respond(data.into()),
|
Ok(data) => responder.respond(data.into()),
|
||||||
Err(e) => warn!("{e}"),
|
Err(e) => warn!("{}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,6 @@ pub mod auth;
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod fetch_object;
|
pub mod fetch_object;
|
||||||
|
pub mod remote;
|
||||||
pub mod requests;
|
pub mod requests;
|
||||||
pub mod server_proto;
|
pub mod server_proto;
|
||||||
pub mod utils;
|
|
||||||
|
|||||||
48
src-tauri/src/remote/remote.rs
Normal file
48
src-tauri/src/remote/remote.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use log::{debug, warn};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::db::{borrow_db_mut_checked, save_db},
|
||||||
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
AppState, AppStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DropHealthcheck {
|
||||||
|
app_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_remote_logic(
|
||||||
|
url: String,
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> Result<(), RemoteAccessError> {
|
||||||
|
debug!("connecting to url {}", url);
|
||||||
|
let base_url = Url::parse(&url)?;
|
||||||
|
|
||||||
|
// Test Drop url
|
||||||
|
let test_endpoint = base_url.join("/api/v1")?;
|
||||||
|
let response = reqwest::blocking::get(test_endpoint.to_string())?;
|
||||||
|
|
||||||
|
let result: DropHealthcheck = response.json()?;
|
||||||
|
|
||||||
|
if result.app_name != "Drop" {
|
||||||
|
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
||||||
|
return Err(RemoteAccessError::InvalidEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut app_state = state.lock().unwrap();
|
||||||
|
app_state.status = AppStatus::SignedOut;
|
||||||
|
drop(app_state);
|
||||||
|
|
||||||
|
let mut db_state = borrow_db_mut_checked();
|
||||||
|
db_state.base_url = base_url.to_string();
|
||||||
|
drop(db_state);
|
||||||
|
|
||||||
|
save_db();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
use reqwest::{Client, RequestBuilder};
|
use reqwest::blocking::{Client, RequestBuilder};
|
||||||
|
|
||||||
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
|
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
|
||||||
|
|
||||||
pub async fn make_request<T: AsRef<str>, F: AsyncFnOnce(RequestBuilder) -> RequestBuilder>(
|
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
path_components: &[T],
|
path_components: &[T],
|
||||||
query: &[(T, T)],
|
query: &[(T, T)],
|
||||||
f: F,
|
f: F,
|
||||||
) -> Result<RequestBuilder, RemoteAccessError> {
|
) -> Result<RequestBuilder, RemoteAccessError> {
|
||||||
let mut base_url = DB.fetch_base_url().await;
|
let mut base_url = DB.fetch_base_url();
|
||||||
for endpoint in path_components {
|
for endpoint in path_components {
|
||||||
base_url = base_url.join(endpoint.as_ref())?;
|
base_url = base_url.join(endpoint.as_ref())?;
|
||||||
}
|
}
|
||||||
@ -19,5 +19,5 @@ pub async fn make_request<T: AsRef<str>, F: AsyncFnOnce(RequestBuilder) -> Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let response = client.get(base_url);
|
let response = client.get(base_url);
|
||||||
Ok(f(response).await)
|
Ok(f(response))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
use http::{
|
||||||
use reqwest::Client;
|
uri::PathAndQuery,
|
||||||
|
Request, Response, StatusCode, Uri,
|
||||||
|
};
|
||||||
|
use reqwest::blocking::Client;
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
use crate::database::db::borrow_db_checked;
|
use crate::database::db::borrow_db_checked;
|
||||||
|
|
||||||
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
let four_oh_four = Response::builder()
|
let four_oh_four = Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.body(Vec::new())
|
.body(Vec::new())
|
||||||
@ -14,8 +17,8 @@ pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder:
|
|||||||
responder.respond(four_oh_four);
|
responder.respond(four_oh_four);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
let db_handle = borrow_db_checked().await;
|
let db_handle = borrow_db_checked();
|
||||||
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
None => return,
|
None => return,
|
||||||
@ -26,14 +29,18 @@ pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriScheme
|
|||||||
|
|
||||||
let mut new_uri = request.uri().clone().into_parts();
|
let mut new_uri = request.uri().clone().into_parts();
|
||||||
new_uri.path_and_query =
|
new_uri.path_and_query =
|
||||||
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).unwrap());
|
Some(PathAndQuery::from_str(&format!("{}?noWrapper=true", path)).unwrap());
|
||||||
new_uri.authority = remote_uri.authority().cloned();
|
new_uri.authority = remote_uri.authority().cloned();
|
||||||
new_uri.scheme = remote_uri.scheme().cloned();
|
new_uri.scheme = remote_uri.scheme().cloned();
|
||||||
let new_uri = Uri::from_parts(new_uri).unwrap();
|
let new_uri = Uri::from_parts(new_uri).unwrap();
|
||||||
|
|
||||||
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
|
let whitelist_prefix = vec!["/store", "/api", "/_", "/fonts"];
|
||||||
|
|
||||||
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
|
if whitelist_prefix
|
||||||
|
.iter()
|
||||||
|
.map(|f| !path.starts_with(f))
|
||||||
|
.all(|f| f)
|
||||||
|
{
|
||||||
webbrowser::open(&new_uri.to_string()).unwrap();
|
webbrowser::open(&new_uri.to_string()).unwrap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,14 +48,13 @@ pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriScheme
|
|||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let response = client
|
let response = client
|
||||||
.request(request.method().clone(), new_uri.to_string())
|
.request(request.method().clone(), new_uri.to_string())
|
||||||
.header("Authorization", format!("Bearer {web_token}"))
|
.header("Authorization", format!("Bearer {}", web_token))
|
||||||
.headers(request.headers().clone())
|
.headers(request.headers().clone())
|
||||||
.send()
|
.send()
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let response_status = response.status();
|
let response_status = response.status();
|
||||||
let response_body = response.bytes().await.unwrap();
|
let response_body = response.bytes().unwrap();
|
||||||
|
|
||||||
let http_response = Response::builder()
|
let http_response = Response::builder()
|
||||||
.status(response_status)
|
.status(response_status)
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
use log::{debug, info, warn};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppStatus, DropFunctionState
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct DropHealthcheck {
|
|
||||||
app_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_remote_logic(
|
|
||||||
url: String,
|
|
||||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
|
||||||
) -> Result<(), RemoteAccessError> {
|
|
||||||
info!("connecting to url {url}");
|
|
||||||
let base_url = Url::parse(&url)?;
|
|
||||||
|
|
||||||
// Test Drop url
|
|
||||||
let test_endpoint = base_url.join("/api/v1")?;
|
|
||||||
let response = reqwest::get(test_endpoint.to_string()).await?;
|
|
||||||
|
|
||||||
let result: DropHealthcheck = response.json().await?;
|
|
||||||
|
|
||||||
if result.app_name != "Drop" {
|
|
||||||
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
|
||||||
return Err(RemoteAccessError::InvalidEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut app_state = state.lock().await;
|
|
||||||
app_state.status = AppStatus::SignedOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut db_state = borrow_db_mut_checked().await;
|
|
||||||
db_state.base_url = base_url.to_string();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||||
"productName": "Drop Desktop Client",
|
"productName": "Drop Desktop Client",
|
||||||
"version": "0.3.0-rc-8",
|
"version": "0.3.0-rc-3",
|
||||||
"identifier": "dev.drop.app",
|
"identifier": "dev.drop.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn dev --port 1432",
|
"beforeDevCommand": "yarn dev --port 1432",
|
||||||
@ -11,11 +11,7 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null,
|
"csp": null
|
||||||
"assetProtocol": {
|
|
||||||
"enable": true,
|
|
||||||
"scope": {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|||||||
2
types.ts
2
types.ts
@ -59,13 +59,11 @@ export enum GameStatusEnum {
|
|||||||
Uninstalling = "Uninstalling",
|
Uninstalling = "Uninstalling",
|
||||||
SetupRequired = "SetupRequired",
|
SetupRequired = "SetupRequired",
|
||||||
Running = "Running",
|
Running = "Running",
|
||||||
PartiallyInstalled = "PartiallyInstalled"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameStatus = {
|
export type GameStatus = {
|
||||||
type: GameStatusEnum;
|
type: GameStatusEnum;
|
||||||
version_name?: string;
|
version_name?: string;
|
||||||
install_dir?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum DownloadableType {
|
export enum DownloadableType {
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@ -1508,11 +1508,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.1.tgz#dc49d899fb873b96ee1d46a171384625ba5ad404"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.1.tgz#dc49d899fb873b96ee1d46a171384625ba5ad404"
|
||||||
integrity sha512-eoQWT+Tq1qSwQpHV+nw1eNYe5B/nm1PoRjQCRiEOS12I1b+X4PUcREfXVX8dPcBT6GrzWGDtaecY0+1p0Rfqlw==
|
integrity sha512-eoQWT+Tq1qSwQpHV+nw1eNYe5B/nm1PoRjQCRiEOS12I1b+X4PUcREfXVX8dPcBT6GrzWGDtaecY0+1p0Rfqlw==
|
||||||
|
|
||||||
"@tauri-apps/api@^2.6.0":
|
|
||||||
version "2.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.6.0.tgz#efd873bf04b0d72cea81f9397e16218f5deafe0f"
|
|
||||||
integrity sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==
|
|
||||||
|
|
||||||
"@tauri-apps/cli-darwin-arm64@2.0.1":
|
"@tauri-apps/cli-darwin-arm64@2.0.1":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.1.tgz#5816c0099977f705d1a7249822fa51f5d3c3750a"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.1.tgz#5816c0099977f705d1a7249822fa51f5d3c3750a"
|
||||||
@ -1593,13 +1588,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@tauri-apps/api" "^2.0.0"
|
"@tauri-apps/api" "^2.0.0"
|
||||||
|
|
||||||
"@tauri-apps/plugin-opener@^2.4.0":
|
|
||||||
version "2.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-opener/-/plugin-opener-2.4.0.tgz#57eae5998e1c396791af16832a9dde16eca06439"
|
|
||||||
integrity sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==
|
|
||||||
dependencies:
|
|
||||||
"@tauri-apps/api" "^2.6.0"
|
|
||||||
|
|
||||||
"@tauri-apps/plugin-os@~2":
|
"@tauri-apps/plugin-os@~2":
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz#ef5511269f59c0ccc580a9d09600034cfaa9743b"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz#ef5511269f59c0ccc580a9d09600034cfaa9743b"
|
||||||
|
|||||||
Reference in New Issue
Block a user