Compare commits

..

44 Commits

Author SHA1 Message Date
f560a62c8f Download fixes (#63)
* refactor: Rename StoredManifest to DropData

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

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

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

* chore: Didn't import debug macro

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

* fix: Download chunks with wrong indexes

Migrated to using checksums as indexes instead

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

* feat: Resume download button

Also added DBWrite and DBRead structs to make database management easier

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

* feat: Download resuming

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

* feat: Resume button and PartiallyInstalled status

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

* feat: Download validation

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

* chore: Ran cargo fix & cargo fmt

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

* fix: download validation, installs, etc

* chore: version bump

---------

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

Signed-off-by: quexeky <git@quexeky.dev>
2025-06-06 12:09:44 +10:00
86bce1c68d Release: v0.3.0-rc-3 (#51) 2025-06-06 09:25:44 +10:00
924e4e334c Database not being properly serialised with rpm_serde (#48)
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-05 17:22:22 +10:00
065eb2356a fix: database corrupted on every startup (#40) 2025-06-01 19:53:24 +10:00
689e9ad890 fix: add new dependencies to linux build 2025-05-28 20:51:32 +10:00
7c35ed73aa fix: Folders can now be copied too
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:48:34 +10:00
8f261a5dac chore: Add extract() function
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:48:34 +10:00
67b6f2aa2e chore: Initial path normalisation & parsing with backup generation
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:47:43 +10:00
11e2b3fe8a fix: regenerate lockfile 2025-05-28 20:37:26 +10:00
eba224f998 fix: remove memd-exec dependency 2025-05-28 20:22:18 +10:00
d045385a5d build: 0.3.0-rc-2 2025-05-28 20:09:58 +10:00
d878806ade Merge branch 'compat' into develop
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 11:24:30 +10:00
b71081006e refactor: Reorganise file structure
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 11:19:48 +10:00
c9e1ed78eb refactor: Delete downloadable_metadata.rs
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 09:18:33 +10:00
446aa70b0b inprogress: compat 2025-05-28 09:07:09 +10:00
1d0b81078a feat: Add "NO_TRAY_ICON" env option
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-27 12:13:49 +10:00
5251a56c3c feat: add arm linux builds 2025-05-25 11:46:24 +10:00
eeca8a7a98 chore(tailscale): Add test
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-16 15:01:50 +10:00
365cdaf311 feat(tailscale): Add TailscaleListener and TailscaleConn
Needs testing on a native windows machine

Signed-off-by: quexeky <git@quexeky.dev>
2025-05-16 12:57:11 +10:00
2957773179 fix: remove reqwest default-features to compile without openssl 2025-05-15 21:03:31 +10:00
15e5fe4dc0 fix: move to reqwest rustls 2025-05-15 20:58:48 +10:00
2dc0a78354 fix: update cargo lock 2025-05-15 20:53:04 +10:00
51c480f245 feat: inline capability registration 2025-05-15 16:05:34 +10:00
95d223e2b2 feat(tailscale): Add wrapper around libtailscale with Tailscale struct
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-15 15:23:20 +10:00
790e8c2afe feat: move to native_model to allow for database upgrades 2025-05-15 10:13:24 +10:00
02edb2cbc1 chore: libtailscale rust build 2025-05-14 10:01:26 +10:00
4b4c0734ec fix: windows builds 2025-05-10 16:41:37 +10:00
e75e0044fb fix: windows launching 2025-05-10 15:38:20 +10:00
65561abdab fix: update object id paths for new server 2025-05-10 15:25:40 +10:00
fed3e08dce fix: re-add minimise button 2025-05-10 14:22:21 +10:00
b0b1e397b1 fix: install dir flow 2025-05-10 09:02:59 +10:00
7b443818d1 fix: github ci and dialogtitle issue 2025-05-08 08:25:10 +10:00
fa4a881cc0 fix: bump tauri.config.json to 0.3.0-rc-1 2025-05-07 14:36:54 +10:00
4f16a6e6b2 fix: remove nightly trigger
it creates releases
2025-05-07 14:29:27 +10:00
47d9e9949b feat: bump app versions to 0.3.0-rc-1 2025-05-07 14:28:11 +10:00
a643d6102b fix: switch to rust nightly 2025-05-07 13:51:27 +10:00
a71ff160c2 feat: add github build 2025-05-07 12:21:03 +10:00
a53a566792 feat: cleanup settings menu and fix styles 2025-05-01 13:36:52 +10:00
84 changed files with 7268 additions and 3754 deletions

70
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: 'publish'
on:
workflow_dispatch: {}
release:
types: [published]
# 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.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.
# 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-release v__VERSION__'
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
releaseDraft: false
prerelease: true
args: ${{ matrix.args }}

6
.gitmodules vendored
View File

@ -1,3 +1,9 @@
[submodule "drop-base"] [submodule "drop-base"]
path = drop-base path = drop-base
url = https://github.com/drop-oss/drop-base url = https://github.com/drop-oss/drop-base
[submodule "src-tauri/tailscale/libtailscale"]
path = src-tauri/tailscale/libtailscale
url = https://github.com/tailscale/libtailscale.git
[submodule "src-tauri/umu/umu-launcher"]
path = src-tauri/umu/umu-launcher
url = https://github.com/Open-Wine-Components/umu-launcher.git

View File

@ -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 Wiki](https://wiki.droposs.org/guides/quickstart.html) The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
## Current features ## Current features
Currently supported are the following features: Currently supported are the following features:

View File

@ -1,78 +1,52 @@
<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 <button type="button" @click="() => buttonActions[props.status.type]()" :class="[
type="button" styles[props.status.type],
@click="() => buttonActions[props.status.type]()" showDropdown ? 'rounded-l-md' : 'rounded-md',
:class="[ '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',
styles[props.status.type], ]">
showDropdown ? 'rounded-l-md' : 'rounded-md', <component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }} {{ buttonNames[props.status.type] }}
</button> </button>
<Menu <Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<div class="h-full"> <div class="h-full">
<MenuButton <MenuButton :class="[
: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 <transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
enter-active-class="transition ease-out duration-100" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
enter-from-class="transform opacity-0 scale-95" leave-from-class="transform opacity-100 scale-100" leave-to-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 <button @click="() => emit('options')" :class="[
@click="() => emit('options')" active
:class="[ ? 'bg-zinc-800 text-zinc-100 outline-none'
active : 'text-zinc-400',
? 'bg-zinc-800 text-zinc-100 outline-none' 'w-full block px-4 py-2 text-sm inline-flex justify-between',
: 'text-zinc-400', ]">
'w-full block px-4 py-2 text-sm inline-flex justify-between', Options
]" <Cog6ToothIcon class="size-5" />
> </button>
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
<MenuItem v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button <button @click="() => emit('uninstall')" :class="[
@click="() => emit('uninstall')" active
:class="[ ? 'bg-zinc-800 text-zinc-100 outline-none'
active : 'text-zinc-400',
? 'bg-zinc-800 text-zinc-100 outline-none' 'w-full block px-4 py-2 text-sm inline-flex justify-between',
: 'text-zinc-400', ]">
'w-full block px-4 py-2 text-sm inline-flex justify-between', Uninstall
]" <TrashIcon class="size-5" />
> </button>
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@ -103,12 +77,14 @@ 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 } = {
@ -128,6 +104,8 @@ const styles: { [key in GameStatusEnum]: string } = {
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", "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 } = {
@ -139,6 +117,7 @@ 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 } = {
@ -150,6 +129,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Updating]: ArrowDownTrayIcon, [GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon, [GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: PlayIcon, [GameStatusEnum.Running]: PlayIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
}; };
const buttonActions: { [key in GameStatusEnum]: () => void } = { const buttonActions: { [key in GameStatusEnum]: () => void } = {
@ -159,7 +139,8 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.SetupRequired]: () => emit("launch"), [GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"), [GameStatusEnum.Installed]: () => emit("launch"),
[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>

View File

@ -42,7 +42,7 @@
</ol> </ol>
</div> </div>
</div> </div>
<WindowControl class="h-16 w-16 p-4" /> <WindowControl />
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<button class="transition h-10 w-10 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-2"> <button class="transition h-full aspect-square text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-[1.1rem]">
<slot /> <slot />
</button> </button>
</template> </template>

View File

@ -87,7 +87,7 @@ router.afterEach(() => {
const state = useAppState(); const state = useAppState();
const profilePictureUrl: string = await useObject( const profilePictureUrl: string = await useObject(
state.value.user?.profilePicture ?? "" state.value.user?.profilePictureObjectId ?? ""
); );
const adminUrl: string = await invoke("gen_drop_url", { const adminUrl: string = await invoke("gen_drop_url", {
path: "/admin", path: "/admin",
@ -99,11 +99,6 @@ function navigate(close: () => any, to: NavigationItem) {
} }
const navigation: NavigationItem[] = [ const navigation: NavigationItem[] = [
{
label: "Account settings",
route: "/account",
prefix: "",
},
{ {
label: "App settings", label: "App settings",
route: "/settings", route: "/settings",

View File

@ -136,15 +136,16 @@ async function auth() {
} }
function authWrapper_wrapper() { function authWrapper_wrapper() {
error.value = undefined;
loading.value = true; loading.value = true;
auth().catch((e) => { auth().catch((e) => {
loading.value = false; loading.value = false;
error.value = e; error.value = e;
if(offerManualTimeout) clearTimeout(offerManualTimeout); if (offerManualTimeout) clearTimeout(offerManualTimeout);
}); });
offerManualTimeout = setTimeout(() => { offerManualTimeout = setTimeout(() => {
offerManual.value = true; offerManual.value = true;
}, 10000); }, 2000);
} }
async function continueManual() { async function continueManual() {

View File

@ -76,6 +76,7 @@ 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",
@ -86,6 +87,7 @@ 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();
@ -107,7 +109,7 @@ async function calculateGames() {
} }
for (const game of rawGames.value) { for (const game of rawGames.value) {
if (icons[game.id]) continue; if (icons[game.id]) continue;
icons[game.id] = await useObject(game.mIconId); icons[game.id] = await useObject(game.mIconObjectId);
} }
} }

View File

@ -1,4 +1,7 @@
<template> <template>
<HeaderButton v-if="showMinimise" @click="() => minimise()">
<MinusIcon />
</HeaderButton>
<HeaderButton @click="() => close()"> <HeaderButton @click="() => close()">
<XMarkIcon /> <XMarkIcon />
</HeaderButton> </HeaderButton>
@ -8,11 +11,14 @@
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid"; import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
async function close(){ const window = getCurrentWindow();
console.log(window); const showMinimise = await window.isMinimizable();
const result = await window.close();
console.log(`closed window: ${result}`); async function close() {
await window.close();
} }
const window = getCurrentWindow(); async function minimise() {
await window.minimize();
}
</script> </script>

View File

@ -1,9 +1,11 @@
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");
@ -15,8 +17,9 @@ export function setupHooks() {
); );
}); });
listen("auth/finished", (event) => { listen("auth/finished", async (event) => {
router.push("/store"); router.push("/store");
state.value = JSON.parse(await invoke("fetch_state"));
}); });
listen("download_error", (event) => { listen("download_error", (event) => {

View File

@ -1,7 +1,7 @@
{ {
"name": "drop-app", "name": "drop-app",
"private": true, "private": true,
"version": "0.2.0-beta", "version": "0.3.0-rc-5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
@ -37,7 +37,9 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"sass-embedded": "^1.79.4", "sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13" "tailwindcss": "^3.4.13",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@ -1,72 +0,0 @@
<template>
<div class="mx-auto max-w-7xl px-8">
<div class="border-b border-zinc-700 py-5">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
Account
</h3>
</div>
<div class="mt-5">
<div class="divide-y divide-zinc-700">
<div class="py-6">
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Sign out of your Drop account on this device
</p>
</div>
<button
@click="signOut"
type="button"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
>
Sign out
</button>
</div>
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ error }}
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { listen } from '@tauri-apps/api/event'
import { useRouter } from '#imports'
import { XCircleIcon } from "@heroicons/vue/16/solid";
const router = useRouter()
const error = ref<string | null>(null)
// Listen for auth events
onMounted(async () => {
await listen('auth/signedout', () => {
router.push('/auth/signedout')
})
})
async function signOut() {
try {
error.value = null
await invoke('sign_out')
} catch (e) {
error.value = `Failed to sign out: ${e}`
}
}
</script>

View File

@ -32,6 +32,7 @@
@uninstall="() => uninstall()" @uninstall="() => uninstall()"
@kill="() => kill()" @kill="() => kill()"
@options="() => (configureModalOpen = true)" @options="() => (configureModalOpen = true)"
@resume="() => resumeDownload()"
:status="status" :status="status"
/> />
<a <a
@ -156,9 +157,9 @@
<template #default> <template #default>
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left"> <div class="mt-3 text-center sm:mt-0 sm:text-left">
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100" <h3 class="text-base font-semibold text-zinc-100"
>Install {{ game.mName }}? >Install {{ game.mName }}?
</DialogTitle> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-zinc-400"> <p class="text-sm text-zinc-400">
Drop will add {{ game.mName }} to the queue to be downloaded. Drop will add {{ game.mName }} to the queue to be downloaded.
@ -350,7 +351,7 @@
<LoadingButton <LoadingButton
@click="() => install()" @click="() => install()"
:disabled=" :disabled="
!(versionOptions && versionOptions.length > 0 && !installDir) !(versionOptions && versionOptions.length > 0)
" "
:loading="installLoading" :loading="installLoading"
type="submit" type="submit"
@ -477,11 +478,11 @@ const remoteUrl: string = await invoke("gen_drop_url", {
path: `/store/${game.value.id}`, path: `/store/${game.value.id}`,
}); });
const bannerUrl = await useObject(game.value.mBannerId); const bannerUrl = await useObject(game.value.mBannerObjectId);
// Get all available images // Get all available images
const mediaUrls = await Promise.all( const mediaUrls = await Promise.all(
game.value.mImageCarousel.map((id) => useObject(id)) game.value.mImageCarouselObjectIds.map((id) => useObject(id))
); );
const htmlDescription = micromark(game.value.mDescription); const htmlDescription = micromark(game.value.mDescription);
@ -495,6 +496,7 @@ 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;
@ -532,6 +534,15 @@ 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 });

View File

@ -91,7 +91,12 @@
<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 type { DownloadableMetadata, Game, GameStatus } from "~/types"; import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
// const actionNames = {
// [GameStatusEnum.Downloading]: "downloading",
// [GameStatusEnum.Verifying]: "verifying",
// }
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => { window.addEventListener("resize", (event) => {
@ -158,7 +163,7 @@ function loadGamesForQueue(v: typeof queue.value) {
if (games.value[id]) return; if (games.value[id]) return;
(async () => { (async () => {
const gameData = await useGame(id); const gameData = await useGame(id);
const cover = await useObject(gameData.game.mCoverId); const cover = await useObject(gameData.game.mCoverObjectId);
games.value[id] = { ...gameData, cover }; games.value[id] = { ...gameData, cover };
})(); })();
} }

View File

@ -45,6 +45,7 @@ import type { Component } from "vue";
import type { NavigationItem } from "~/types"; import type { NavigationItem } from "~/types";
import { platform } from '@tauri-apps/plugin-os'; import { platform } from '@tauri-apps/plugin-os';
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { UserIcon } from "@heroicons/vue/20/solid";
const systemData = await invoke<{ const systemData = await invoke<{
clientId: string; clientId: string;
@ -101,6 +102,12 @@ const navigation = computed(() => [
prefix: "/settings/downloads", prefix: "/settings/downloads",
icon: ArrowDownTrayIcon, icon: ArrowDownTrayIcon,
}, },
{
label: "Account",
route: "/settings/account",
prefix: "/settings/account",
icon: UserIcon
},
...(isDebugMode.value ? [{ ...(isDebugMode.value ? [{
label: "Debug Info", label: "Debug Info",
route: "/settings/debug", route: "/settings/debug",

View File

@ -0,0 +1,64 @@
<template>
<div class="border-b border-zinc-700 py-5">
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
General
</h3>
</div>
<div class="mt-5 flex flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
<p class="mt-1 text-sm leading-6 text-zinc-400">
Sign out of your Drop account on this device
</p>
</div>
<button
@click="signOut"
type="button"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
>
Sign out
</button>
</div>
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ error }}
</h3>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useRouter } from "#imports";
import { XCircleIcon } from "@heroicons/vue/16/solid";
const router = useRouter();
const error = ref<string | null>(null);
// Listen for auth events
onMounted(async () => {
await listen("auth/signedout", () => {
router.push("/auth/signedout");
});
});
async function signOut() {
try {
error.value = null;
await invoke("sign_out");
} catch (e) {
error.value = `Failed to sign out: ${e}`;
}
}
</script>

View File

@ -1,8 +1,15 @@
<template> <template>
<div> <div class="border-b border-zinc-700 py-5">
<div class="border-b border-zinc-600 py-2 px-1"> <h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
Downloads
</h3>
</div>
<div class="mt-5">
<div class="border-b border-zinc-600">
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"> <div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
<div class="ml-4 mt-2"> <div class="ml-4 mt-2 pb-4">
<h3 class="text-base font-display font-semibold text-zinc-100"> <h3 class="text-base font-display font-semibold text-zinc-100">
Install directories Install directories
</h3> </h3>

View File

@ -1,60 +1,59 @@
<template> <template>
<div class="divide-y divide-zinc-700"> <div class="border-b border-zinc-700 py-5">
<div class="py-6"> <h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
<h2 class="text-base font-semibold font-display leading-7 text-zinc-100">General</h2> General
<p class="mt-1 text-sm leading-6 text-zinc-400"> </h3>
Configure basic application settings </div>
</p>
<div class="mt-10 space-y-8"> <div class="mt-5 space-y-8">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div> <div>
<h3 class="text-sm font-medium leading-6 text-zinc-100">Start with system</h3> <h3 class="text-sm font-medium leading-6 text-zinc-100">
<p class="mt-1 text-sm leading-6 text-zinc-400"> Start with system
Drop will automatically start when you log into your computer </h3>
</p> <p class="mt-1 text-sm leading-6 text-zinc-400">
</div> Drop will automatically start when you log into your computer
<Switch </p>
v-model="autostartEnabled" </div>
:class="[ <Switch
autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700', v-model="autostartEnabled"
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out' :class="[
]" autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700',
> 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out',
<span ]"
:class="[ >
autostartEnabled ? 'translate-x-5' : 'translate-x-0', <span
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out' :class="[
]" autostartEnabled ? 'translate-x-5' : 'translate-x-0',
/> 'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
</Switch> ]"
</div> />
</div> </Switch>
</div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Switch } from '@headlessui/vue' import { Switch } from "@headlessui/vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
defineProps<{}>() defineProps<{}>();
const autostartEnabled = ref<boolean>(false) const autostartEnabled = ref<boolean>(false);
// Load initial state // Load initial state
invoke('get_autostart_enabled').then((enabled) => { invoke("get_autostart_enabled").then((enabled) => {
autostartEnabled.value = enabled as boolean autostartEnabled.value = enabled as boolean;
}) });
// Watch for changes and update autostart // Watch for changes and update autostart
watch(autostartEnabled, async (newValue: boolean) => { watch(autostartEnabled, async (newValue: boolean) => {
try { try {
await invoke('toggle_autostart', { enabled: newValue }) await invoke("toggle_autostart", { enabled: newValue });
} catch (error) { } catch (error) {
console.error('Failed to toggle autostart:', error) console.error("Failed to toggle autostart:", error);
// Revert the toggle if it failed // Revert the toggle if it failed
autostartEnabled.value = !newValue autostartEnabled.value = !newValue;
} }
}) });
</script> </script>

2492
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.2.0-beta-prerelease-1" version = "0.3.0-rc-5"
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 = "2021" edition = "2021"
@ -16,8 +16,6 @@ tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "drop_app_lib" name = "drop_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build]
rustflags = ["-C", "target-feature=+aes,+sse2"] rustflags = ["-C", "target-feature=+aes,+sse2"]
@ -50,7 +48,6 @@ throttle_my_fn = "0.2.6"
parking_lot = "0.12.3" parking_lot = "0.12.3"
atomic-instant-full = "0.1.0" atomic-instant-full = "0.1.0"
cacache = "13.1.0" cacache = "13.1.0"
bincode = "1.3.3"
http-serde = "2.1.1" http-serde = "2.1.1"
reqwest-middleware = "0.4.0" reqwest-middleware = "0.4.0"
reqwest-middleware-cache = "0.1.1" reqwest-middleware-cache = "0.1.1"
@ -69,6 +66,8 @@ whoami = "1.6.0"
filetime = "0.2.25" filetime = "0.2.25"
walkdir = "2.5.0" walkdir = "2.5.0"
known-folders = "1.2.0" known-folders = "1.2.0"
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
# tailscale = { path = "./tailscale" }
[dependencies.dynfmt] [dependencies.dynfmt]
version = "0.1.5" version = "0.1.5"
@ -76,7 +75,7 @@ features = ["curly"]
[dependencies.tauri] [dependencies.tauri]
version = "2.1.1" version = "2.1.1"
features = ["tray-icon"] features = ["protocol-asset", "tray-icon"]
[dependencies.tokio] [dependencies.tokio]
version = "1.40.0" version = "1.40.0"
@ -96,11 +95,12 @@ features = ["v4", "fast-rng", "macro-diagnostics"]
[dependencies.rustbreak] [dependencies.rustbreak]
version = "2" version = "2"
features = [] # 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"
features = ["json", "blocking"] default-features = false
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
[dependencies.serde] [dependencies.serde]
version = "1" version = "1"

View File

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

View File

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

View File

@ -1,7 +1,9 @@
use std::{ use std::{
fs::{self, create_dir_all, File}, fs::{self, create_dir_all, File},
io::{self, ErrorKind, Read, Write}, io::{self, ErrorKind, Read, Write},
path::{Path, PathBuf}, thread::sleep, time::Duration, path::{Path, PathBuf},
thread::sleep,
time::Duration,
}; };
use super::{ use super::{
@ -60,7 +62,6 @@ pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
file.id = Some(id); file.id = Some(id);
} }
let binding = serde_json::to_string(meta).unwrap(); let binding = serde_json::to_string(meta).unwrap();
println!("Binding: {}", &binding);
let serialized = binding.as_bytes(); let serialized = binding.as_bytes();
let mut file = tempfile().unwrap(); let mut file = tempfile().unwrap();
file.write(serialized).unwrap(); file.write(serialized).unwrap();
@ -117,8 +118,10 @@ pub fn extract(file: PathBuf) -> Result<(), BackupError> {
let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?; let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?;
create_dir_all(&new_path.parent().unwrap()).unwrap(); create_dir_all(&new_path.parent().unwrap()).unwrap();
println!("Current path {:?} copying to {:?}", &current_path, &new_path); println!(
"Current path {:?} copying to {:?}",
&current_path, &new_path
);
copy_item(current_path, new_path).unwrap(); copy_item(current_path, new_path).unwrap();
} }
@ -168,7 +171,11 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
let metadata = entry.metadata()?; let metadata = entry.metadata()?;
if metadata.is_file() { if metadata.is_file() {
debug!("Writing file {} to {}", entry_path.display(), dest_entry_path.display()); debug!(
"Writing file {} to {}",
entry_path.display(),
dest_entry_path.display()
);
fs::copy(&entry_path, &dest_entry_path)?; fs::copy(&entry_path, &dest_entry_path)?;
} else if metadata.is_dir() { } else if metadata.is_dir() {
copy_dir_recursive(&entry_path, &dest_entry_path)?; copy_dir_recursive(&entry_path, &dest_entry_path)?;

View File

@ -7,13 +7,13 @@ use std::{
use serde_json::Value; use serde_json::Value;
use crate::{ use crate::{
database::{db::borrow_db_mut_checked, settings::Settings}, database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
download_manager::internal_error::InternalError,
}; };
use super::{ use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, db::{borrow_db_checked, DATA_ROOT_DIR},
debug::SystemData, debug::SystemData,
models::data::Settings,
}; };
// Will, in future, return disk/remaining size // Will, in future, return disk/remaining size
@ -28,12 +28,10 @@ pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
pub fn delete_download_dir(index: usize) { pub fn delete_download_dir(index: usize) {
let mut lock = borrow_db_mut_checked(); 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 fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> { 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() {
@ -60,7 +58,6 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
} }
lock.applications.install_dirs.push(new_dir); lock.applications.install_dirs.push(new_dir);
drop(lock); drop(lock);
save_db();
Ok(()) Ok(())
} }
@ -74,8 +71,6 @@ pub fn update_settings(new_settings: Value) {
} }
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 fn fetch_settings() -> Settings { pub fn fetch_settings() -> Settings {

View File

@ -1,150 +1,44 @@
use std::{ use std::{
collections::HashMap,
fs::{self, create_dir_all}, fs::{self, create_dir_all},
hash::Hash, mem::ManuallyDrop,
path::{Path, PathBuf}, ops::{Deref, DerefMut},
path::PathBuf,
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
}; };
use chrono::Utc; use chrono::Utc;
use log::{debug, error, info}; use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_with::serde_as;
use tauri::AppHandle;
use url::Url; use url::Url;
use crate::{ use crate::DB;
database::settings::Settings,
download_manager::downloadable_metadata::DownloadableMetadata,
games::{library::push_game_update, state::GameStatusManager},
process::process_manager::Platform,
DB,
};
#[derive(serde::Serialize, Clone, Deserialize)] use super::models::data::Database;
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Running {},
}
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String,
pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
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>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
impl Database {
fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
}
}
}
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> = pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::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)]
pub struct DropDatabaseSerializer; pub struct DropDatabaseSerializer;
impl<T: Serialize + DeserializeOwned> DeSerializer<T> for DropDatabaseSerializer { impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string())) native_model::rmp_serde_1_3::RmpSerde::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string()))
} }
fn deserialize<R: std::io::Read>(&self, s: R) -> rustbreak::error::DeSerResult<T> { fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string())) let mut buf = Vec::new();
s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
.map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
} }
} }
@ -163,12 +57,14 @@ impl DatabaseImpls for DatabaseInterface {
let games_base_dir = data_root_dir.join("games"); let games_base_dir = data_root_dir.join("games");
let logs_root_dir = data_root_dir.join("logs"); let logs_root_dir = data_root_dir.join("logs");
let cache_dir = data_root_dir.join("cache"); 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.clone()).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();
create_dir_all(&pfx_dir).unwrap();
let exists = fs::exists(db_path.clone()).unwrap(); let exists = fs::exists(db_path.clone()).unwrap();
@ -206,6 +102,7 @@ fn handle_invalid_database(
games_base_dir: PathBuf, games_base_dir: PathBuf,
cache_dir: PathBuf, cache_dir: PathBuf,
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> { ) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
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();
@ -227,9 +124,46 @@ fn handle_invalid_database(
PathDatabase::create_at_path(db_path, db).expect("Database could not be created") PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
} }
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> { // To automatically save the database upon drop
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = RwLockWriteGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = RwLockReadGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Drop for DBWrite<'a> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() { match DB.borrow_data() {
Ok(data) => data, Ok(data) => DBRead(data),
Err(e) => { Err(e) => {
error!("database borrow failed with error {}", e); error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {}", e); panic!("database borrow failed with error {}", e);
@ -237,22 +171,12 @@ pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
} }
} }
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> { pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() { match DB.borrow_data_mut() {
Ok(data) => data, Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => { Err(e) => {
error!("database borrow mut failed with error {}", e); error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {}", e); panic!("database borrow mut failed with error {}", e);
} }
} }
} }
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}

View File

@ -1,4 +1,4 @@
pub mod commands; pub mod commands;
pub mod db; pub mod db;
pub mod debug; pub mod debug;
pub mod settings; pub mod models;

View File

@ -0,0 +1,347 @@
use crate::database::models::data::Database;
pub mod data {
use std::path::PathBuf;
use native_model::{native_model, Model};
use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion;
pub type Database = v3::Database;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v2::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::{collections::HashMap, process::Command};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
pub mod v1 {
use crate::process::process_manager::Platform;
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use super::*;
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 2, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String,
pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 1, 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>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 4, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool, // ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false,
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 1, 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,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Running {},
}
#[derive(serde::Serialize, Clone, Deserialize)]
#[native_model(id = 6, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
#[native_model(id = 8, version = 1)]
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy,
)]
pub enum DownloadType {
Game,
Tool,
DLC,
Mod,
}
#[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}
#[native_model(id = 1, version = 1)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
}
pub mod v2 {
use std::{collections::HashMap, path::PathBuf};
use serde_with::serde_as;
use super::*;
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: v1::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo {
pub umu_installed: bool,
}
impl From<v1::Database> for Database {
fn from(value: v1::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: crate::database::models::Database::create_new_compat_info(),
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
fn from(value: v1::GameDownloadStatus) -> Self {
match value {
v1::GameDownloadStatus::Remote {} => Self::Remote {},
v1::GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Self::SetupRequired {
version_name,
install_dir,
},
v1::GameDownloadStatus::Installed {
version_name,
install_dir,
} => Self::Installed {
version_name,
install_dir,
},
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
Self {
game_statuses: value
.game_statuses
.into_iter()
.map(|x| (x.0, x.1.into()))
.collect::<HashMap<String, GameDownloadStatus>>(),
install_dirs: value.install_dirs,
game_versions: value.game_versions,
installed_game_version: value.installed_game_version,
transient_statuses: value.transient_statuses,
}
}
}
}
mod v3 {
use std::path::PathBuf;
use super::*;
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
impl From<v2::Database> for Database {
fn from(value: v2::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications.into(),
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
}

View File

@ -1,26 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false
}
}
}
// Ideally use pointers instead of a macro to assign the settings
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
// where T: for<'a> Deserialize<'a>
// {
// *t = serde_json::from_value(v)?;
// Ok(())
// }

View File

@ -1,6 +1,6 @@
use std::sync::Mutex; use std::sync::Mutex;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState}; use crate::{database::models::data::DownloadableMetadata, AppState};
#[tauri::command] #[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) { pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {

View File

@ -12,12 +12,14 @@ use std::{
use log::{debug, info}; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use crate::error::application_download_error::ApplicationDownloadError; use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
};
use super::{ use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent}, download_manager_builder::{CurrentProgressObject, DownloadAgent},
downloadable_metadata::DownloadableMetadata, util::queue::Queue,
queue::Queue,
}; };
pub enum DownloadManagerSignal { pub enum DownloadManagerSignal {
@ -70,6 +72,7 @@ impl Serialize for DownloadManagerStatus {
pub enum DownloadStatus { pub enum DownloadStatus {
Queued, Queued,
Downloading, Downloading,
Validating,
Error, Error,
} }
@ -167,10 +170,7 @@ impl DownloadManager {
self.command_sender self.command_sender
.send(DownloadManagerSignal::UpdateUIQueue) .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap(); .unwrap();
self.command_sender self.command_sender.send(DownloadManagerSignal::Go).unwrap();
.send(DownloadManagerSignal::Go)
.unwrap();
} }
pub fn pause_downloads(&self) { pub fn pause_downloads(&self) {
self.command_sender self.command_sender

View File

@ -11,17 +11,19 @@ use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use crate::{ use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError, error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}, games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
}; };
use super::{ use super::{
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
downloadable::Downloadable, downloadable::Downloadable,
downloadable_metadata::DownloadableMetadata, util::{
progress_object::ProgressObject, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
queue::Queue, progress_object::ProgressObject,
queue::Queue,
},
}; };
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>; pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
@ -209,7 +211,9 @@ impl DownloadManagerBuilder {
} }
if self.current_download_agent.is_some() { if self.current_download_agent.is_some() {
if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() { if self.download_queue.read().front().unwrap()
== &self.current_download_agent.as_ref().unwrap().metadata()
{
debug!( debug!(
"Current download agent: {:?}", "Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata() self.current_download_agent.as_ref().unwrap().metadata()
@ -244,13 +248,30 @@ impl DownloadManagerBuilder {
// 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());
download_agent.on_complete(&app_handle); match download_agent.validate() {
sender Ok(true) => {
.send(DownloadManagerSignal::Completed(download_agent.metadata())) download_agent.on_complete(&app_handle);
.unwrap(); sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
}
Ok(false) => {
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!(
"download {:?} has validation error {}",
download_agent.metadata(),
&e
);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
} }
// Ok(false) is for incomplete but exited properly // Ok(false) 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);
} }
Err(e) => { Err(e) => {

View File

@ -2,17 +2,21 @@ use std::sync::Arc;
use tauri::AppHandle; use tauri::AppHandle;
use crate::error::application_download_error::ApplicationDownloadError; use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
};
use super::{ use super::{
download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl, download_manager::DownloadStatus,
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
}; };
pub trait Downloadable: Send + Sync { pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>; fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>; fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl; fn control_flag(&self) -> DownloadThreadControl;
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
fn status(&self) -> DownloadStatus; fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata; fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle); fn on_initialised(&self, app_handle: &AppHandle);

View File

@ -1,26 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
pub enum DownloadType {
Game,
Tool,
DLC,
Mod,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}

View File

@ -1,27 +0,0 @@
use std::{fmt::Display, io, sync::mpsc::SendError};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum InternalError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for InternalError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InternalError::IOError(error) => write!(f, "{}", error),
InternalError::SignalError(send_error) => write!(f, "{}", send_error),
}
}
}
impl<T> From<SendError<T>> for InternalError<T> {
fn from(value: SendError<T>) -> Self {
InternalError::SignalError(value)
}
}
impl<T> From<io::Error> for InternalError<T> {
fn from(value: io::Error) -> Self {
InternalError::IOError(value)
}
}

View File

@ -1,10 +1,5 @@
pub mod commands; pub mod commands;
pub mod download_manager; pub mod download_manager;
pub mod download_manager_builder; pub mod download_manager_builder;
pub mod download_thread_control_flag;
pub mod downloadable; pub mod downloadable;
pub mod downloadable_metadata; pub mod util;
pub mod internal_error;
pub mod progress_object;
pub mod queue;
pub mod rolling_progress_updates;

View File

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

View File

@ -10,9 +10,9 @@ use std::{
use atomic_instant_full::AtomicInstant; use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle; use throttle_my_fn::throttle;
use super::{ use crate::download_manager::download_manager::DownloadManagerSignal;
download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
}; use super::rolling_progress_updates::RollingProgressWindow;
#[derive(Clone)] #[derive(Clone)]
pub struct ProgressObject { pub struct ProgressObject {
@ -84,6 +84,17 @@ 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()
} }
@ -122,7 +133,11 @@ 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 - current_bytes_downloaded; // bytes let bytes_remaining = if (max < current_bytes_downloaded) {
0
} else {
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);

View File

@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use super::downloadable_metadata::DownloadableMetadata; use crate::database::models::data::DownloadableMetadata;
#[derive(Clone)] #[derive(Clone)]
pub struct Queue { pub struct Queue {

View File

@ -30,4 +30,9 @@ 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));
}
} }

View File

@ -25,7 +25,7 @@ impl Display for ApplicationDownloadError {
ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", 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, "{}", error), ApplicationDownloadError::IoError(error) => write!(f, "io error: {}", 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"),
} }
} }

View File

@ -6,7 +6,7 @@ use serde_with::SerializeDisplay;
pub enum BackupError { pub enum BackupError {
InvalidSystem, InvalidSystem,
NotFound, NotFound,
ParseError ParseError,
} }
impl Display for BackupError { impl Display for BackupError {

View File

@ -0,0 +1,27 @@
use std::{fmt::Display, io, sync::mpsc::SendError};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum DownloadManagerError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for DownloadManagerError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadManagerError::IOError(error) => write!(f, "{}", error),
DownloadManagerError::SignalError(send_error) => write!(f, "{}", send_error),
}
}
}
impl<T> From<SendError<T>> for DownloadManagerError<T> {
fn from(value: SendError<T>) -> Self {
DownloadManagerError::SignalError(value)
}
}
impl<T> From<io::Error> for DownloadManagerError<T> {
fn from(value: io::Error) -> Self {
DownloadManagerError::IOError(value)
}
}

View File

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

View File

@ -1,5 +1,4 @@
use std::{ use std::{
any::{Any, TypeId},
error::Error, error::Error,
fmt::{Display, Formatter}, fmt::{Display, Formatter},
sync::Arc, sync::Arc,
@ -23,7 +22,6 @@ pub enum RemoteAccessError {
ManifestDownloadFailed(StatusCode, String), ManifestDownloadFailed(StatusCode, String),
OutOfSync, OutOfSync,
Cache(cacache::Error), Cache(cacache::Error),
Generic(String),
} }
impl Display for RemoteAccessError { impl Display for RemoteAccessError {
@ -59,7 +57,6 @@ impl Display for RemoteAccessError {
status, response 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::Generic(message) => write!(f, "{}", message),
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error), RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
} }
} }

View File

@ -11,7 +11,7 @@ pub struct Collection {
name: String, name: String,
is_default: bool, is_default: bool,
user_id: String, user_id: String,
entries: Vec<CollectionObject> entries: Vec<CollectionObject>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Default)] #[derive(Serialize, Deserialize, Debug, Clone, Default)]
@ -21,4 +21,3 @@ pub struct CollectionObject {
game_id: String, game_id: String,
game: Game, game: Game,
} }

View File

@ -2,7 +2,12 @@ use reqwest::blocking::Client;
use serde_json::json; use serde_json::json;
use url::Url; use url::Url;
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, DB}; use crate::{
database::db::DatabaseImpls,
error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request},
DB,
};
use super::collection::{Collection, Collections}; use super::collection::{Collection, Collections};
@ -20,9 +25,12 @@ pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
#[tauri::command] #[tauri::command]
pub 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(&client, &["/api/v1/client/collection/", &collection_id], &[], |r| { let response = make_request(
r.header("Authorization", generate_authorization_header()) &client,
})? &["/api/v1/client/collection/", &collection_id],
&[],
|r| r.header("Authorization", generate_authorization_header()),
)?
.send()?; .send()?;
Ok(response.json()?) Ok(response.json()?)
@ -35,7 +43,6 @@ pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError>
let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?; 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()) .header("Authorization", generate_authorization_header())
@ -46,9 +53,16 @@ pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError>
} }
#[tauri::command] #[tauri::command]
pub fn add_game_to_collection(collection_id: String, game_id: String) -> Result<(), RemoteAccessError> { pub fn add_game_to_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = Client::new(); let client = Client::new();
let url = Url::parse(&format!("{}api/v1/client/collection/{}/entry/", DB.fetch_base_url(), collection_id))?; let url = Url::parse(&format!(
"{}api/v1/client/collection/{}/entry/",
DB.fetch_base_url(),
collection_id
))?;
client client
.post(url) .post(url)
@ -61,7 +75,11 @@ pub fn add_game_to_collection(collection_id: String, game_id: String) -> Result<
#[tauri::command] #[tauri::command]
pub fn delete_collection(collection_id: String) -> Result<bool, 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!("{}api/v1/client/collection/{}", DB.fetch_base_url(), collection_id))?; let base_url = Url::parse(&format!(
"{}api/v1/client/collection/{}",
DB.fetch_base_url(),
collection_id
))?;
let response = client let response = client
.delete(base_url) .delete(base_url)
@ -71,9 +89,16 @@ pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessErro
Ok(response.json()?) Ok(response.json()?)
} }
#[tauri::command] #[tauri::command]
pub fn delete_game_in_collection(collection_id: String, game_id: String) -> Result<(), RemoteAccessError> { pub fn delete_game_in_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = Client::new(); let client = Client::new();
let base_url = Url::parse(&format!("{}api/v1/client/collection/{}/entry", DB.fetch_base_url(), collection_id))?; let base_url = Url::parse(&format!(
"{}api/v1/client/collection/{}/entry",
DB.fetch_base_url(),
collection_id
))?;
client client
.delete(base_url) .delete(base_url)

View File

@ -1,2 +1,2 @@
pub mod commands;
pub mod collection; pub mod collection;
pub mod commands;

View File

@ -1,9 +1,15 @@
use std::sync::Mutex; use std::sync::Mutex;
use tauri::{AppHandle, Manager}; use tauri::AppHandle;
use crate::{ use crate::{
database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta, uninstall_game_logic}, offline, AppState 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,
uninstall_game_logic,
},
offline, AppState,
}; };
use super::{ use super::{
@ -15,16 +21,30 @@ use super::{
}; };
#[tauri::command] #[tauri::command]
pub fn fetch_library(state: tauri::State<'_, Mutex<AppState>>) -> Result<Vec<Game>, RemoteAccessError> { pub fn fetch_library(
offline!(state, fetch_library_logic, fetch_library_logic_offline, state) state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<Game>, RemoteAccessError> {
offline!(
state,
fetch_library_logic,
fetch_library_logic_offline,
state
)
} }
#[tauri::command] #[tauri::command]
pub fn fetch_game( pub fn fetch_game(
game_id: String, game_id: String,
state: tauri::State<'_, Mutex<AppState>> state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
offline!(state, fetch_game_logic, fetch_game_logic_offline, game_id, state) let res = offline!(
state,
fetch_game_logic,
fetch_game_logic_offline,
game_id,
state
);
res
} }
#[tauri::command] #[tauri::command]

View File

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

View File

@ -1,70 +1,76 @@
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::database::db::{ use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
borrow_db_checked, ApplicationTransientStatus, DatabaseImpls, use crate::database::models::data::{
GameDownloadStatus, ApplicationTransientStatus, DownloadType, DownloadableMetadata,
}; };
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{ use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::util::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag, DownloadThreadControl, DownloadThreadControlFlag,
}; };
use crate::download_manager::downloadable::Downloadable; use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
use crate::download_manager::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::library::{on_game_complete, push_game_update, GameUpdateEvent}; use crate::games::downloads::validate::game_validate_logic;
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
use crate::remote::requests::make_request; use crate::remote::requests::make_request;
use crate::DB;
use log::{debug, error, info}; use log::{debug, error, info};
use rayon::ThreadPoolBuilder; use rayon::ThreadPoolBuilder;
use slice_deque::SliceDeque; use std::collections::HashMap;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, OpenOptions};
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant; use std::time::Instant;
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use urlencoding::encode;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use rustix::fs::{fallocate, FallocateFlags}; use rustix::fs::{fallocate, FallocateFlags};
use super::download_logic::download_game_chunk; use super::download_logic::download_game_chunk;
use super::stored_manifest::StoredManifest; use super::drop_data::DropData;
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>>,
completed_contexts: Mutex<SliceDeque<usize>>, context_map: Mutex<HashMap<String, bool>>,
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: StoredManifest, pub stored_manifest: DropData,
status: Mutex<DownloadStatus>, status: Mutex<DownloadStatus>,
} }
impl GameDownloadAgent { impl GameDownloadAgent {
pub fn new( pub fn new_from_index(
id: String, id: String,
version: String, version: String,
target_download_dir: usize, target_download_dir: usize,
sender: Sender<DownloadManagerSignal>, sender: Sender<DownloadManagerSignal>,
) -> Self { ) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let db_lock = borrow_db_checked(); let db_lock = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock); drop(db_lock);
Self::new(id, version, base_dir, sender)
}
pub fn new(
id: String,
version: String,
base_dir: PathBuf,
sender: Sender<DownloadManagerSignal>,
) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let base_dir_path = Path::new(&base_dir); let 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 =
StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone()); DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
Self { Self {
id, id,
@ -72,7 +78,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()),
completed_contexts: Mutex::new(SliceDeque::new()), context_map: Mutex::new(HashMap::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,
@ -177,11 +183,15 @@ 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() {
return Ok(()); self.generate_contexts()?;
} }
self.generate_contexts()?; self.context_map
.lock()
.unwrap()
.extend(self.stored_manifest.get_contexts());
Ok(()) Ok(())
} }
@ -193,20 +203,18 @@ 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 = File::create(path.clone()).unwrap(); let file = OpenOptions::new()
.read(true)
.write(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() {
@ -229,6 +237,14 @@ 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(())
@ -247,13 +263,14 @@ impl GameDownloadAgent {
.build() .build()
.unwrap(); .unwrap();
let completed_indexes = Arc::new(boxcar::Vec::new()); let completed_contexts = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_indexes.clone(); let completed_indexes_loop_arc = completed_contexts.clone();
let contexts = self.contexts.lock().unwrap(); let contexts = self.contexts.lock().unwrap();
debug!("{:#?}", contexts); debug!("{:#?}", contexts);
pool.scope(|scope| { pool.scope(|scope| {
let client = &reqwest::blocking::Client::new(); let client = &reqwest::blocking::Client::new();
let context_map = self.context_map.lock().unwrap();
for (index, context) in contexts.iter().enumerate() { for (index, context) in contexts.iter().enumerate() {
let client = client.clone(); let client = client.clone();
let completed_indexes = completed_indexes_loop_arc.clone(); let completed_indexes = completed_indexes_loop_arc.clone();
@ -262,7 +279,7 @@ impl GameDownloadAgent {
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 self.completed_contexts.lock().unwrap().contains(&index) { if Some(&true) == context_map.get(&context.checksum) {
progress_handle.skip(context.length); progress_handle.skip(context.length);
continue; continue;
} }
@ -278,7 +295,7 @@ impl GameDownloadAgent {
("name", &context.file_name), ("name", &context.file_name),
("chunk", &context.index.to_string()), ("chunk", &context.index.to_string()),
], ],
|r| r.header("Authorization", generate_authorization_header()), |r| r,
) { ) {
Ok(request) => request, Ok(request) => request,
Err(e) => { Err(e) => {
@ -294,10 +311,18 @@ impl GameDownloadAgent {
scope.spawn(move |_| { scope.spawn(move |_| {
match download_game_chunk(context, &self.control_flag, progress_handle, request) match download_game_chunk(context, &self.control_flag, progress_handle, request)
{ {
Ok(res) => { Ok(true) => {
if res { debug!(
completed_indexes.push(index); "Finished context #{} with checksum {}",
} index, context.checksum
);
completed_indexes.push(context.checksum.clone());
}
Ok(false) => {
debug!(
"Didn't finish context #{} with checksum {}",
index, context.checksum
);
} }
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);
@ -308,36 +333,46 @@ impl GameDownloadAgent {
} }
}); });
let newly_completed = completed_indexes.to_owned(); let newly_completed = completed_contexts.to_owned();
let completed_lock_len = { let completed_lock_len = {
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap(); let mut context_map_lock = self.context_map.lock().unwrap();
for (_, item) in newly_completed.iter() { for (_, item) in newly_completed.iter() {
completed_contexts_lock.push_front(*item); context_map_lock.insert(item.clone(), true);
} }
completed_contexts_lock.len() context_map_lock.values().filter(|x| **x).count()
}; };
let context_map_lock = self.context_map.lock().unwrap();
let contexts = contexts
.iter()
.map(|x| {
(
x.checksum.clone(),
context_map_lock
.get(&x.checksum)
.cloned()
.or(Some(false))
.unwrap(),
)
})
.collect::<Vec<(String, bool)>>();
drop(context_map_lock);
// If we're not out of contexts, we're not done, so we don't fire completed self.stored_manifest.set_contexts(&contexts);
if completed_lock_len != contexts.len() { 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)
} }
} }
@ -376,8 +411,11 @@ impl Downloadable for GameDownloadAgent {
error!("error while managing download: {}", error); error!("error while managing download: {}", error);
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = borrow_db_mut_checked();
handle.applications.transient_statuses.remove(&self.metadata()); handle
.applications
.transient_statuses
.remove(&self.metadata());
} }
fn on_complete(&self, app_handle: &tauri::AppHandle) { fn on_complete(&self, app_handle: &tauri::AppHandle) {
@ -391,18 +429,12 @@ impl Downloadable for GameDownloadAgent {
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly // TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
fn on_incomplete(&self, app_handle: &tauri::AppHandle) { fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata(); on_game_incomplete(
*self.status.lock().unwrap() = DownloadStatus::Queued; &self.metadata(),
app_handle self.stored_manifest.base_path.to_string_lossy().to_string(),
.emit( app_handle,
&format!("update_game/{}", meta.id), )
GameUpdateEvent { .unwrap();
game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None),
version: None,
},
)
.unwrap();
} }
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {} fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
@ -410,4 +442,15 @@ impl Downloadable for GameDownloadAgent {
fn status(&self) -> DownloadStatus { fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone() self.status.lock().unwrap().clone()
} }
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating;
game_validate_logic(
&self.stored_manifest,
self.contexts.lock().unwrap().clone(),
self.progress.clone(),
self.sender.clone(),
&self.control_flag,
)
}
} }

View File

@ -1,16 +1,18 @@
use crate::download_manager::download_thread_control_flag::{ use crate::download_manager::util::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag, DownloadThreadControl, DownloadThreadControlFlag,
}; };
use crate::download_manager::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::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext; use crate::games::downloads::manifest::DropDownloadContext;
use log::warn; use crate::remote::auth::generate_authorization_header;
use log::{debug, info, warn};
use md5::{Context, Digest}; use md5::{Context, Digest};
use reqwest::blocking::{RequestBuilder, Response}; use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions}; use std::fs::{set_permissions, Permissions};
use std::io::{ErrorKind, Read}; use std::io::{ErrorKind, Read};
use std::os::unix::fs::MetadataExt;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::{ use std::{
@ -92,19 +94,32 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
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 {
buf_writer.flush()?;
return Ok(false); return Ok(false);
} }
let bytes_read = self.source.read(&mut copy_buf)?; let mut bytes_read = self.source.read(&mut copy_buf)?;
current_size += bytes_read; current_size += bytes_read;
if current_size > self.size {
let over = current_size - self.size;
warn!("server sent too many bytes... {} over", over);
bytes_read -= over;
current_size = self.size;
}
buf_writer.write_all(&copy_buf[0..bytes_read])?; buf_writer.write_all(&copy_buf[0..bytes_read])?;
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;
} }
} }
buf_writer.flush()?;
Ok(true) Ok(true)
} }
@ -121,17 +136,24 @@ pub fn download_game_chunk(
progress: ProgressHandle, progress: ProgressHandle,
request: RequestBuilder, request: RequestBuilder,
) -> Result<bool, ApplicationDownloadError> { ) -> Result<bool, ApplicationDownloadError> {
debug!(
"Starting download chunk {}, {}, {} #{}",
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
);
// If we're paused // If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop { if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0); progress.set(0);
return Ok(false); return Ok(false);
} }
let request = request.header("Authorization", generate_authorization_header());
let response = request let response = request
.header("Authorization", generate_authorization_header())
.send() .send()
.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 err = response.json().unwrap();
return Err(ApplicationDownloadError::Communication( return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err), RemoteAccessError::InvalidResponse(err),
@ -154,13 +176,14 @@ pub fn download_game_chunk(
)); ));
} }
let mut pipeline = DropDownloadPipeline::new( let length = content_length.unwrap().try_into().unwrap();
response,
destination, if length != ctx.length {
control_flag, return Err(ApplicationDownloadError::DownloadError);
progress, }
content_length.unwrap().try_into().unwrap(),
); let mut pipeline =
DropDownloadPipeline::new(response, destination, control_flag, progress, length);
let completed = pipeline let completed = pipeline
.copy() .copy()
@ -185,5 +208,10 @@ pub 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)
} }

View File

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

View File

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

View File

@ -1,79 +0,0 @@
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
sync::Mutex,
};
use log::{error, warn};
use serde::{Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
#[derive(Serialize, Deserialize, Debug)]
pub struct StoredManifest {
game_id: String,
game_version: String,
pub completed_contexts: Mutex<Vec<usize>>,
pub base_path: PathBuf,
}
static DROP_DATA_PATH: &str = ".dropdata";
impl StoredManifest {
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
Self {
base_path,
game_id,
game_version,
completed_contexts: Mutex::new(Vec::new()),
}
}
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(_) => return StoredManifest::new(game_id, game_version, base_path),
};
let mut s = Vec::new();
match file.read_to_end(&mut s) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
return StoredManifest::new(game_id, game_version, base_path);
}
};
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
Ok(manifest) => manifest,
Err(e) => {
warn!("{}", e);
StoredManifest::new(game_id, game_version, base_path)
}
}
}
pub fn write(&self) {
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
Ok(json) => json,
Err(_) => return,
};
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(e) => {
error!("{}", e);
return;
}
};
match file.write_all(&manifest_raw) {
Ok(_) => {}
Err(e) => error!("{}", e),
};
}
pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
*self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec<usize> {
self.completed_contexts.lock().unwrap().clone()
}
}

View File

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

View File

@ -2,24 +2,25 @@ use std::fs::remove_dir_all;
use std::sync::Mutex; use std::sync::Mutex;
use std::thread::spawn; use std::thread::spawn;
use log::{debug, error, info, warn}; use log::{debug, error, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use tauri::Emitter; use tauri::Emitter;
use tauri::{AppHandle, Manager};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus}; use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
};
use crate::download_manager::download_manager::DownloadStatus; use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::error::library_error::LibraryError; use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::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::{AppState, DB}; use crate::AppState;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct { pub struct FetchGameStruct {
game: Game, game: Game,
status: GameStatusWithTransient, status: GameStatusWithTransient,
@ -35,11 +36,11 @@ pub struct Game {
m_description: String, m_description: String,
// mDevelopers // mDevelopers
// mPublishers // mPublishers
m_icon_id: String, m_icon_object_id: String,
m_banner_id: String, m_banner_object_id: String,
m_cover_id: String, m_cover_object_id: String,
m_image_library: Vec<String>, m_image_library_object_ids: Vec<String>,
m_image_carousel: Vec<String>, m_image_carousel_object_ids: Vec<String>,
} }
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent { pub struct GameUpdateEvent {
@ -143,7 +144,7 @@ pub fn fetch_game_logic(
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let mut state_handle = state.lock().unwrap(); let mut state_handle = state.lock().unwrap();
let handle = DB.borrow_data().unwrap(); let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id); let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option { let version = match metadata_option {
@ -219,7 +220,7 @@ pub fn fetch_game_logic_offline(
id: String, id: String,
_state: tauri::State<'_, Mutex<AppState>>, _state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let handle = DB.borrow_data().unwrap(); let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id); let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option { let version = match metadata_option {
None => None, None => None,
@ -312,6 +313,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
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
@ -340,7 +345,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
.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();
@ -353,6 +357,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
); );
} }
}); });
} else {
warn!("invalid previous state for uninstall, failing silently.")
} }
} }
@ -364,6 +370,66 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
.cloned() .cloned()
} }
pub fn on_game_incomplete(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", generate_authorization_header()),
)?
.send()?;
let game_version: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
.entry(meta.id.clone())
.or_default()
.insert(meta.version.clone().unwrap(), game_version.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
let status = GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.clone().unwrap(),
install_dir,
};
handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(handle);
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
},
)
.unwrap();
Ok(())
}
pub fn on_game_complete( pub fn on_game_complete(
meta: &DownloadableMetadata, meta: &DownloadableMetadata,
install_dir: String, install_dir: String,
@ -403,7 +469,6 @@ pub 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 {
@ -423,7 +488,6 @@ pub fn on_game_complete(
.game_statuses .game_statuses
.insert(meta.id.clone(), status.clone()); .insert(meta.id.clone(), status.clone());
drop(db_handle); drop(db_handle);
save_db();
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
@ -438,7 +502,12 @@ pub fn on_game_complete(
Ok(()) Ok(())
} }
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, version: Option<GameVersion>, status: GameStatusWithTransient) { pub fn push_game_update(
app_handle: &AppHandle,
game_id: &String,
version: Option<GameVersion>,
status: GameStatusWithTransient,
) {
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", game_id), &format!("update_game/{}", game_id),
@ -462,7 +531,7 @@ pub fn update_game_configuration(
game_id: String, game_id: String,
options: FrontendGameOptions, options: FrontendGameOptions,
) -> Result<(), LibraryError> { ) -> Result<(), LibraryError> {
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = borrow_db_mut_checked();
let installed_version = handle let installed_version = handle
.applications .applications
.installed_game_version .installed_game_version
@ -493,8 +562,5 @@ pub fn update_game_configuration(
.unwrap() .unwrap()
.insert(version.to_string(), existing_configuration); .insert(version.to_string(), existing_configuration);
drop(handle);
save_db();
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
pub mod collections;
pub mod commands; pub mod commands;
pub mod downloads; pub mod downloads;
pub mod library; pub mod library;
pub mod state; pub mod state;
pub mod collections;

View File

@ -1,4 +1,7 @@
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus}; use crate::database::{
db::borrow_db_checked,
models::data::{ApplicationTransientStatus, GameDownloadStatus},
};
pub type GameStatusWithTransient = ( pub type GameStatusWithTransient = (
Option<GameDownloadStatus>, Option<GameDownloadStatus>,

View File

@ -1,27 +1,24 @@
mod database; mod database;
mod games; mod games;
mod autostart; mod client;
mod cleanup;
mod cloud_saves;
mod commands;
mod download_manager; mod download_manager;
mod error; mod error;
mod process; mod process;
mod remote; mod remote;
use crate::database::db::DatabaseImpls; use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use autostart::{get_autostart_enabled, toggle_autostart}; use client::commands::fetch_state;
use cleanup::{cleanup_and_exit, quit}; use client::{
use cloud_saves::resolver::test; autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
use commands::fetch_state; cleanup::{cleanup_and_exit, quit},
};
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::{ use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, DATA_ROOT_DIR};
borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR, 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,
}; };
@ -36,8 +33,6 @@ use games::commands::{
}; };
use games::downloads::commands::download_game; use games::downloads::commands::download_game;
use games::library::{update_game_configuration, Game}; use games::library::{update_game_configuration, Game};
use http::Response;
use http::{header::*, response::Builder as ResponseBuilder};
use log::{debug, info, warn, LevelFilter}; use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
@ -52,9 +47,7 @@ use remote::commands::{
sign_out, use_remote, sign_out, use_remote,
}; };
use remote::fetch_object::{fetch_object, fetch_object_offline}; use remote::fetch_object::{fetch_object, fetch_object_offline};
use remote::requests::make_request;
use remote::server_proto::{handle_server_proto, handle_server_proto_offline}; use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
use reqwest::blocking::Body;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::env; use std::env;
use std::path::Path; use std::path::Path;
@ -88,10 +81,9 @@ pub struct User {
username: String, username: String,
admin: bool, admin: bool,
display_name: String, display_name: String,
profile_picture: String, profile_picture_object_id: String,
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AppState<'a> { pub struct AppState<'a> {
@ -106,7 +98,6 @@ pub struct AppState<'a> {
} }
fn setup(handle: AppHandle) -> AppState<'static> { fn setup(handle: AppHandle) -> AppState<'static> {
test();
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}",
@ -164,8 +155,9 @@ fn setup(handle: AppHandle) -> AppState<'static> {
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 {
database::db::GameDownloadStatus::Remote {} => {} GameDownloadStatus::Remote {} => {}
database::db::GameDownloadStatus::SetupRequired { GameDownloadStatus::PartiallyInstalled { .. } => {}
GameDownloadStatus::SetupRequired {
version_name: _, version_name: _,
install_dir, install_dir,
} => { } => {
@ -174,7 +166,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id); missing_games.push(game_id);
} }
} }
database::db::GameDownloadStatus::Installed { GameDownloadStatus::Installed {
version_name: _, version_name: _,
install_dir, install_dir,
} => { } => {
@ -202,7 +194,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
debug!("finished setup!"); debug!("finished setup!");
// Sync autostart state // Sync autostart state
if let Err(e) = autostart::sync_autostart_on_startup(&handle) { if let Err(e) = sync_autostart_on_startup(&handle) {
warn!("failed to sync autostart state: {}", e); warn!("failed to sync autostart state: {}", e);
} }
@ -268,6 +260,7 @@ 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,
@ -291,10 +284,9 @@ pub fn run() {
debug!("initialized drop client"); debug!("initialized drop client");
app.manage(Mutex::new(state)); app.manage(Mutex::new(state));
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{ {
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?; let _ = app.deep_link().register_all();
debug!("registered all pre-defined deep links"); debug!("registered all pre-defined deep links");
} }
@ -337,23 +329,25 @@ pub fn run() {
], ],
)?; )?;
TrayIconBuilder::new() run_on_tray(|| {
.icon(app.default_window_icon().unwrap().clone()) TrayIconBuilder::new()
.menu(&menu) .icon(app.default_window_icon().unwrap().clone())
.on_menu_event(|app, event| match event.id.as_ref() { .menu(&menu)
"open" => { .on_menu_event(|app, event| match event.id.as_ref() {
app.webview_windows().get("main").unwrap().show().unwrap(); "open" => {
} app.webview_windows().get("main").unwrap().show().unwrap();
"quit" => { }
cleanup_and_exit(app, &app.state()); "quit" => {
} cleanup_and_exit(app, &app.state());
}
_ => { _ => {
warn!("menu event not handled: {:?}", event.id); warn!("menu event not handled: {:?}", event.id);
} }
}) })
.build(app) .build(app)
.expect("error while setting up tray menu"); .expect("error while setting up tray menu");
});
{ {
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
@ -397,12 +391,13 @@ pub fn run() {
request, request,
responder responder
); );
}) })
.on_window_event(|window, event| { .on_window_event(|window, event| {
if let WindowEvent::CloseRequested { api, .. } = event { if let WindowEvent::CloseRequested { api, .. } = event {
window.hide().unwrap(); run_on_tray(|| {
api.prevent_close(); window.hide().unwrap();
api.prevent_close();
});
} }
}) })
.build(tauri::generate_context!()) .build(tauri::generate_context!())
@ -410,9 +405,20 @@ pub fn run() {
app.run(|_app_handle, event| { app.run(|_app_handle, event| {
if let RunEvent::ExitRequested { code, api, .. } = event { if let RunEvent::ExitRequested { code, api, .. } = event {
if code.is_none() { run_on_tray(|| {
api.prevent_exit(); if code.is_none() {
} api.prevent_exit();
}
});
} }
}); });
} }
fn run_on_tray<T: FnOnce() -> ()>(f: T) {
if match std::env::var("NO_TRAY_ICON") {
Ok(s) => s.to_lowercase() != "true",
Err(_) => true,
} {
(f)();
}
}

View File

@ -1,13 +1 @@
// Since this code isn't being used, we can either:
// 1. Delete the entire file if compatibility features are not planned
// 2. Or add a TODO comment if planning to implement later
// Option 1: Delete the file
// Delete src-tauri/src/process/compat.rs
// Option 2: Add TODO comment
/*
TODO: Compatibility layer for running Windows games on Linux
This module is currently unused but reserved for future implementation
of Windows game compatibility features on Linux.
*/

View File

@ -1,3 +1,4 @@
pub mod commands; pub mod commands;
#[cfg(target_os = "linux")]
pub mod compat; pub mod compat;
pub mod process_manager; pub mod process_manager;

View File

@ -1,9 +1,9 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::{File, OpenOptions}, fs::OpenOptions,
io::{self, Error}, io::{self},
path::{Path, PathBuf}, path::PathBuf,
process::{Child, Command, ExitStatus}, process::{Command, ExitStatus},
str::FromStr, str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::spawn, thread::spawn,
@ -15,14 +15,15 @@ use log::{debug, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shared_child::SharedChild; use shared_child::SharedChild;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{ use crate::{
database::db::{ database::{
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, db::{borrow_db_mut_checked, DATA_ROOT_DIR},
DATA_ROOT_DIR, models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
GameVersion,
},
}, },
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError, error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager}, games::{library::push_game_update, state::GameStatusManager},
AppState, DB, AppState, DB,
@ -192,6 +193,10 @@ 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),
}; };
@ -247,7 +252,11 @@ 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::Remote {} => unreachable!("nuh uh"), GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => unreachable!("Game registered as 'Partially Installed'"),
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
}; };
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch); let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);
@ -266,10 +275,13 @@ impl ProcessManager<'_> {
.map_err(|e| ProcessError::FormatError(e.to_string()))? .map_err(|e| ProcessError::FormatError(e.to_string()))?
.to_string(); .to_string();
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let mut command = Command::new("cmd"); let mut command = Command::new("cmd");
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
command.args(["/C", &launch_string]); command.raw_arg(format!("/C \"{}\"", &launch_string));
info!("launching (in {}): {}", install_dir, launch_string,); info!("launching (in {}): {}", install_dir, launch_string,);
@ -342,8 +354,6 @@ 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,
@ -369,7 +379,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!(),
} }
} }
} }
@ -392,14 +402,14 @@ impl ProcessHandler for NativeGameLauncher {
_meta: &DownloadableMetadata, _meta: &DownloadableMetadata,
launch_command: String, launch_command: String,
args: Vec<String>, args: Vec<String>,
game_version: &GameVersion, _game_version: &GameVersion,
current_dir: &str, _current_dir: &str,
) -> String { ) -> String {
format!("\"{}\" {}", launch_command, args.join(" ")) format!("\"{}\" {}", launch_command, args.join(" "))
} }
} }
const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run"; pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
struct UMULauncher; struct UMULauncher;
impl ProcessHandler for UMULauncher { impl ProcessHandler for UMULauncher {
fn create_launch_process( fn create_launch_process(

View File

@ -1,20 +1,20 @@
use std::{env, sync::Mutex}; use std::{collections::HashMap, env, sync::Mutex};
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, warn}; use log::{debug, error, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{ database::{
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls, db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth,
}, },
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User, DB, AppState, AppStatus, User,
}; };
use super::{ use super::{
@ -22,11 +22,16 @@ use super::{
requests::make_request, requests::make_request,
}; };
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CapabilityConfiguration {}
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct InitiateRequestBody { struct InitiateRequestBody {
name: String, name: String,
platform: String, platform: String,
capabilities: HashMap<String, CapabilityConfiguration>,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -58,8 +63,6 @@ pub fn generate_authorization_header() -> String {
} }
pub fn fetch_user() -> Result<User, RemoteAccessError> { pub fn fetch_user() -> Result<User, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let header = generate_authorization_header(); let header = generate_authorization_header();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
@ -106,6 +109,9 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?; 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() {
return Err(RemoteAccessError::InvalidResponse(response.json()?));
}
let response_struct: HandshakeResponse = response.json()?; let response_struct: HandshakeResponse = response.json()?;
{ {
@ -116,8 +122,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
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 = {
@ -134,32 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let mut handle = borrow_db_mut_checked(); 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();
{
let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedIn;
app_state_handle.user = Some(fetch_user()?);
// Setup capabilities
let endpoint = base_url.join("/api/v1/client/capability")?;
let header = generate_authorization_header();
let body = json!({
"capability": "cloudSaves",
"configuration": {}
});
let response = client
.post(endpoint)
.header("Authorization", header)
.json(&body)
.send()?;
if response.status().is_success() {
debug!("registered client for 'cloudSaves' capability")
}
}
Ok(()) Ok(())
} }
@ -175,6 +153,16 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
return; return;
} }
let app_state = app.state::<Mutex<AppState>>();
let mut state_lock = app_state.lock().unwrap();
let (app_status, user) = setup();
state_lock.status = app_status;
state_lock.user = user;
drop(state_lock);
app.emit("auth/finished", ()).unwrap(); app.emit("auth/finished", ()).unwrap();
} }
@ -190,6 +178,10 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
let body = InitiateRequestBody { let body = InitiateRequestBody {
name: format!("{} (Desktop)", hostname.into_string().unwrap()), name: format!("{} (Desktop)", hostname.into_string().unwrap()),
platform: env::consts::OS.to_string(), platform: env::consts::OS.to_string(),
capabilities: HashMap::from([
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
]),
}; };
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();

View File

@ -1,7 +1,5 @@
use std::sync::RwLockReadGuard;
use crate::{ use crate::{
database::db::{borrow_db_checked, Database}, database::{db::borrow_db_checked, models::data::Database},
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
}; };
use cacache::Integrity; use cacache::Integrity;
@ -9,7 +7,6 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response}
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_binary::binary_stream::Endian; 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 ),* ) => {

View File

@ -6,11 +6,16 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, AppState, AppStatus database::db::{borrow_db_checked, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request},
AppState, AppStatus,
}; };
use super::{ use super::{
auth::{auth_initiate_logic, recieve_handshake, setup}, cache::{cache_object, get_cached_object}, remote::use_remote_logic auth::{auth_initiate_logic, recieve_handshake, setup},
cache::{cache_object, get_cached_object},
remote::use_remote_logic,
}; };
#[tauri::command] #[tauri::command]
@ -36,24 +41,22 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
#[tauri::command] #[tauri::command]
pub 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()); let _drop_url = gen_drop_url(path.clone())?;
let req = make_request( let req = make_request(&Client::new(), &[&path], &[], |r| {
&Client::new(), r.header("Authorization", generate_authorization_header())
&[&path], })?
&[], .send();
|r| { r.header("Authorization", generate_authorization_header()) }
)?.send();
match req { match req {
Ok(data) => { Ok(data) => {
let data = data.bytes()?.to_vec(); let data = data.bytes()?.to_vec();
cache_object(&path, &data)?; cache_object(&path, &data)?;
Ok(data) Ok(data)
}, }
Err(e) => { Err(e) => {
debug!("{}", e); debug!("{}", e);
get_cached_object::<&str, Vec<u8>>(&path) get_cached_object::<&str, Vec<u8>>(&path)
}, }
} }
} }
#[tauri::command] #[tauri::command]
@ -62,8 +65,6 @@ pub fn sign_out(app: AppHandle) {
{ {
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
handle.auth = None; handle.auth = None;
drop(handle);
save_db();
} }
// Update app state // Update app state

View File

@ -2,12 +2,13 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::warn; use log::warn;
use tauri::UriSchemeResponder; use tauri::UriSchemeResponder;
use super::{auth::generate_authorization_header, cache::{cache_object, get_cached_object, ObjectCache}, requests::make_request}; use super::{
auth::generate_authorization_header,
cache::{cache_object, get_cached_object, ObjectCache},
requests::make_request,
};
pub fn fetch_object( pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
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..];
@ -25,7 +26,7 @@ pub fn fetch_object(
Ok(data) => responder.respond(data.into()), Ok(data) => responder.respond(data.into()),
Err(e) => { Err(e) => {
warn!("{}", e) warn!("{}", e)
}, }
} }
return; return;
} }
@ -41,10 +42,7 @@ pub fn fetch_object(
responder.respond(resp); responder.respond(resp);
} }
pub fn fetch_object_offline( pub fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
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); let data = get_cached_object::<&str, ObjectCache>(object_id);

View File

@ -5,9 +5,8 @@ use serde::Deserialize;
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_mut_checked, save_db}, database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppState,
error::remote_access_error::RemoteAccessError, AppStatus,
AppState, AppStatus,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -40,9 +39,6 @@ pub fn use_remote_logic(
let mut db_state = borrow_db_mut_checked(); let mut db_state = borrow_db_mut_checked();
db_state.base_url = base_url.to_string(); db_state.base_url = base_url.to_string();
drop(db_state);
save_db();
Ok(()) Ok(())
} }

View File

@ -1,10 +1,6 @@
use std::{path::PathBuf, str::FromStr}; use std::str::FromStr;
use http::{ use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
uri::{Authority, PathAndQuery},
Request, Response, StatusCode, Uri,
};
use log::info;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use tauri::UriSchemeResponder; use tauri::UriSchemeResponder;

7
src-tauri/tailscale/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

294
src-tauri/tailscale/Cargo.lock generated Normal file
View File

@ -0,0 +1,294 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "abs-file-macro"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3909701959b9fcb4d065c3cf3044fe77f6b07a85d748be4630f5214e8d7acc1"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "prettyplease"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tailscale"
version = "0.1.0"
dependencies = [
"abs-file-macro",
"bindgen",
"libc",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"

View File

@ -0,0 +1,11 @@
[package]
name = "tailscale"
version = "0.1.0"
edition = "2024"
[build-dependencies]
bindgen = "*"
abs-file-macro = "0.1.2"
[dependencies]
libc = "0.2.172"

View File

@ -0,0 +1,36 @@
extern crate bindgen;
use abs_file_macro::abs_file;
use std::path::PathBuf;
use std::process::Command;
fn main() {
let build_folder = PathBuf::from(abs_file!());
let build_folder = build_folder.parent().unwrap();
let in_path = build_folder.join("libtailscale");
let out_path = build_folder.join("src/");
let mut make_cmd = Command::new("make");
make_cmd.arg("c-archive");
make_cmd.current_dir(in_path.clone());
make_cmd.status().expect("Make build failed");
let bindings = bindgen::Builder::default()
.header(in_path.join("libtailscale.h").to_str().unwrap())
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
println!("cargo:rerun-if-changed=libtailscale/tailscale.go");
println!(
"cargo:rustc-link-search=native={}",
in_path.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static={}", "tailscale");
}

View File

@ -0,0 +1,363 @@
/* automatically generated by rust-bindgen 0.71.1 */
#[derive(PartialEq, Copy, Clone, Hash, Debug, Default)]
#[repr(C)]
pub struct __BindgenComplex<T> {
pub re: T,
pub im: T,
}
pub const _ERRNO_H: u32 = 1;
pub const _FEATURES_H: u32 = 1;
pub const _DEFAULT_SOURCE: u32 = 1;
pub const __GLIBC_USE_ISOC2Y: u32 = 0;
pub const __GLIBC_USE_ISOC23: u32 = 0;
pub const __USE_ISOC11: u32 = 1;
pub const __USE_ISOC99: u32 = 1;
pub const __USE_ISOC95: u32 = 1;
pub const __USE_POSIX_IMPLICITLY: u32 = 1;
pub const _POSIX_SOURCE: u32 = 1;
pub const _POSIX_C_SOURCE: u32 = 200809;
pub const __USE_POSIX: u32 = 1;
pub const __USE_POSIX2: u32 = 1;
pub const __USE_POSIX199309: u32 = 1;
pub const __USE_POSIX199506: u32 = 1;
pub const __USE_XOPEN2K: u32 = 1;
pub const __USE_XOPEN2K8: u32 = 1;
pub const _ATFILE_SOURCE: u32 = 1;
pub const __WORDSIZE: u32 = 64;
pub const __WORDSIZE_TIME64_COMPAT32: u32 = 0;
pub const __TIMESIZE: u32 = 64;
pub const __USE_TIME_BITS64: u32 = 1;
pub const __USE_MISC: u32 = 1;
pub const __USE_ATFILE: u32 = 1;
pub const __USE_FORTIFY_LEVEL: u32 = 0;
pub const __GLIBC_USE_DEPRECATED_GETS: u32 = 0;
pub const __GLIBC_USE_DEPRECATED_SCANF: u32 = 0;
pub const __GLIBC_USE_C23_STRTOL: u32 = 0;
pub const _STDC_PREDEF_H: u32 = 1;
pub const __STDC_IEC_559__: u32 = 1;
pub const __STDC_IEC_60559_BFP__: u32 = 201404;
pub const __STDC_IEC_559_COMPLEX__: u32 = 1;
pub const __STDC_IEC_60559_COMPLEX__: u32 = 201404;
pub const __STDC_ISO_10646__: u32 = 201706;
pub const __GNU_LIBRARY__: u32 = 6;
pub const __GLIBC__: u32 = 2;
pub const __GLIBC_MINOR__: u32 = 41;
pub const _SYS_CDEFS_H: u32 = 1;
pub const __glibc_c99_flexarr_available: u32 = 1;
pub const __LDOUBLE_REDIRECTS_TO_FLOAT128_ABI: u32 = 0;
pub const __HAVE_GENERIC_SELECTION: u32 = 1;
pub const _BITS_ERRNO_H: u32 = 1;
pub const EPERM: u32 = 1;
pub const ENOENT: u32 = 2;
pub const ESRCH: u32 = 3;
pub const EINTR: u32 = 4;
pub const EIO: u32 = 5;
pub const ENXIO: u32 = 6;
pub const E2BIG: u32 = 7;
pub const ENOEXEC: u32 = 8;
pub const EBADF: u32 = 9;
pub const ECHILD: u32 = 10;
pub const EAGAIN: u32 = 11;
pub const ENOMEM: u32 = 12;
pub const EACCES: u32 = 13;
pub const EFAULT: u32 = 14;
pub const ENOTBLK: u32 = 15;
pub const EBUSY: u32 = 16;
pub const EEXIST: u32 = 17;
pub const EXDEV: u32 = 18;
pub const ENODEV: u32 = 19;
pub const ENOTDIR: u32 = 20;
pub const EISDIR: u32 = 21;
pub const EINVAL: u32 = 22;
pub const ENFILE: u32 = 23;
pub const EMFILE: u32 = 24;
pub const ENOTTY: u32 = 25;
pub const ETXTBSY: u32 = 26;
pub const EFBIG: u32 = 27;
pub const ENOSPC: u32 = 28;
pub const ESPIPE: u32 = 29;
pub const EROFS: u32 = 30;
pub const EMLINK: u32 = 31;
pub const EPIPE: u32 = 32;
pub const EDOM: u32 = 33;
pub const ERANGE: u32 = 34;
pub const EDEADLK: u32 = 35;
pub const ENAMETOOLONG: u32 = 36;
pub const ENOLCK: u32 = 37;
pub const ENOSYS: u32 = 38;
pub const ENOTEMPTY: u32 = 39;
pub const ELOOP: u32 = 40;
pub const EWOULDBLOCK: u32 = 11;
pub const ENOMSG: u32 = 42;
pub const EIDRM: u32 = 43;
pub const ECHRNG: u32 = 44;
pub const EL2NSYNC: u32 = 45;
pub const EL3HLT: u32 = 46;
pub const EL3RST: u32 = 47;
pub const ELNRNG: u32 = 48;
pub const EUNATCH: u32 = 49;
pub const ENOCSI: u32 = 50;
pub const EL2HLT: u32 = 51;
pub const EBADE: u32 = 52;
pub const EBADR: u32 = 53;
pub const EXFULL: u32 = 54;
pub const ENOANO: u32 = 55;
pub const EBADRQC: u32 = 56;
pub const EBADSLT: u32 = 57;
pub const EDEADLOCK: u32 = 35;
pub const EBFONT: u32 = 59;
pub const ENOSTR: u32 = 60;
pub const ENODATA: u32 = 61;
pub const ETIME: u32 = 62;
pub const ENOSR: u32 = 63;
pub const ENONET: u32 = 64;
pub const ENOPKG: u32 = 65;
pub const EREMOTE: u32 = 66;
pub const ENOLINK: u32 = 67;
pub const EADV: u32 = 68;
pub const ESRMNT: u32 = 69;
pub const ECOMM: u32 = 70;
pub const EPROTO: u32 = 71;
pub const EMULTIHOP: u32 = 72;
pub const EDOTDOT: u32 = 73;
pub const EBADMSG: u32 = 74;
pub const EOVERFLOW: u32 = 75;
pub const ENOTUNIQ: u32 = 76;
pub const EBADFD: u32 = 77;
pub const EREMCHG: u32 = 78;
pub const ELIBACC: u32 = 79;
pub const ELIBBAD: u32 = 80;
pub const ELIBSCN: u32 = 81;
pub const ELIBMAX: u32 = 82;
pub const ELIBEXEC: u32 = 83;
pub const EILSEQ: u32 = 84;
pub const ERESTART: u32 = 85;
pub const ESTRPIPE: u32 = 86;
pub const EUSERS: u32 = 87;
pub const ENOTSOCK: u32 = 88;
pub const EDESTADDRREQ: u32 = 89;
pub const EMSGSIZE: u32 = 90;
pub const EPROTOTYPE: u32 = 91;
pub const ENOPROTOOPT: u32 = 92;
pub const EPROTONOSUPPORT: u32 = 93;
pub const ESOCKTNOSUPPORT: u32 = 94;
pub const EOPNOTSUPP: u32 = 95;
pub const EPFNOSUPPORT: u32 = 96;
pub const EAFNOSUPPORT: u32 = 97;
pub const EADDRINUSE: u32 = 98;
pub const EADDRNOTAVAIL: u32 = 99;
pub const ENETDOWN: u32 = 100;
pub const ENETUNREACH: u32 = 101;
pub const ENETRESET: u32 = 102;
pub const ECONNABORTED: u32 = 103;
pub const ECONNRESET: u32 = 104;
pub const ENOBUFS: u32 = 105;
pub const EISCONN: u32 = 106;
pub const ENOTCONN: u32 = 107;
pub const ESHUTDOWN: u32 = 108;
pub const ETOOMANYREFS: u32 = 109;
pub const ETIMEDOUT: u32 = 110;
pub const ECONNREFUSED: u32 = 111;
pub const EHOSTDOWN: u32 = 112;
pub const EHOSTUNREACH: u32 = 113;
pub const EALREADY: u32 = 114;
pub const EINPROGRESS: u32 = 115;
pub const ESTALE: u32 = 116;
pub const EUCLEAN: u32 = 117;
pub const ENOTNAM: u32 = 118;
pub const ENAVAIL: u32 = 119;
pub const EISNAM: u32 = 120;
pub const EREMOTEIO: u32 = 121;
pub const EDQUOT: u32 = 122;
pub const ENOMEDIUM: u32 = 123;
pub const EMEDIUMTYPE: u32 = 124;
pub const ECANCELED: u32 = 125;
pub const ENOKEY: u32 = 126;
pub const EKEYEXPIRED: u32 = 127;
pub const EKEYREVOKED: u32 = 128;
pub const EKEYREJECTED: u32 = 129;
pub const EOWNERDEAD: u32 = 130;
pub const ENOTRECOVERABLE: u32 = 131;
pub const ERFKILL: u32 = 132;
pub const EHWPOISON: u32 = 133;
pub const ENOTSUP: u32 = 95;
pub type wchar_t = ::std::os::raw::c_uint;
#[repr(C)]
#[repr(align(16))]
#[derive(Debug, Copy, Clone)]
pub struct max_align_t {
pub __clang_max_align_nonce1: ::std::os::raw::c_longlong,
pub __bindgen_padding_0: u64,
pub __clang_max_align_nonce2: u128,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of max_align_t"][::std::mem::size_of::<max_align_t>() - 32usize];
["Alignment of max_align_t"][::std::mem::align_of::<max_align_t>() - 16usize];
["Offset of field: max_align_t::__clang_max_align_nonce1"]
[::std::mem::offset_of!(max_align_t, __clang_max_align_nonce1) - 0usize];
["Offset of field: max_align_t::__clang_max_align_nonce2"]
[::std::mem::offset_of!(max_align_t, __clang_max_align_nonce2) - 16usize];
};
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct _GoString_ {
pub p: *const ::std::os::raw::c_char,
pub n: isize,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of _GoString_"][::std::mem::size_of::<_GoString_>() - 16usize];
["Alignment of _GoString_"][::std::mem::align_of::<_GoString_>() - 8usize];
["Offset of field: _GoString_::p"][::std::mem::offset_of!(_GoString_, p) - 0usize];
["Offset of field: _GoString_::n"][::std::mem::offset_of!(_GoString_, n) - 8usize];
};
unsafe extern "C" {
pub fn __errno_location() -> *mut ::std::os::raw::c_int;
}
pub type GoInt8 = ::std::os::raw::c_schar;
pub type GoUint8 = ::std::os::raw::c_uchar;
pub type GoInt16 = ::std::os::raw::c_short;
pub type GoUint16 = ::std::os::raw::c_ushort;
pub type GoInt32 = ::std::os::raw::c_int;
pub type GoUint32 = ::std::os::raw::c_uint;
pub type GoInt64 = ::std::os::raw::c_longlong;
pub type GoUint64 = ::std::os::raw::c_ulonglong;
pub type GoInt = GoInt64;
pub type GoUint = GoUint64;
pub type GoUintptr = usize;
pub type GoFloat32 = f32;
pub type GoFloat64 = f64;
pub type GoComplex64 = __BindgenComplex<f32>;
pub type GoComplex128 = __BindgenComplex<f64>;
pub type _check_for_64_bit_pointer_matching_GoInt = [::std::os::raw::c_char; 1usize];
pub type GoString = _GoString_;
pub type GoMap = *mut ::std::os::raw::c_void;
pub type GoChan = *mut ::std::os::raw::c_void;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GoInterface {
pub t: *mut ::std::os::raw::c_void,
pub v: *mut ::std::os::raw::c_void,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of GoInterface"][::std::mem::size_of::<GoInterface>() - 16usize];
["Alignment of GoInterface"][::std::mem::align_of::<GoInterface>() - 8usize];
["Offset of field: GoInterface::t"][::std::mem::offset_of!(GoInterface, t) - 0usize];
["Offset of field: GoInterface::v"][::std::mem::offset_of!(GoInterface, v) - 8usize];
};
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GoSlice {
pub data: *mut ::std::os::raw::c_void,
pub len: GoInt,
pub cap: GoInt,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of GoSlice"][::std::mem::size_of::<GoSlice>() - 24usize];
["Alignment of GoSlice"][::std::mem::align_of::<GoSlice>() - 8usize];
["Offset of field: GoSlice::data"][::std::mem::offset_of!(GoSlice, data) - 0usize];
["Offset of field: GoSlice::len"][::std::mem::offset_of!(GoSlice, len) - 8usize];
["Offset of field: GoSlice::cap"][::std::mem::offset_of!(GoSlice, cap) - 16usize];
};
unsafe extern "C" {
pub fn TsnetNewServer() -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetStart(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetUp(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetClose(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetGetIps(
sd: ::std::os::raw::c_int,
buf: *mut ::std::os::raw::c_char,
buflen: usize,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetErrmsg(
sd: ::std::os::raw::c_int,
buf: *mut ::std::os::raw::c_char,
buflen: usize,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetListen(
sd: ::std::os::raw::c_int,
network: *mut ::std::os::raw::c_char,
addr: *mut ::std::os::raw::c_char,
listenerOut: *mut ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetGetRemoteAddr(
listener: ::std::os::raw::c_int,
conn: ::std::os::raw::c_int,
buf: *mut ::std::os::raw::c_char,
buflen: usize,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetDial(
sd: ::std::os::raw::c_int,
network: *mut ::std::os::raw::c_char,
addr: *mut ::std::os::raw::c_char,
connOut: *mut ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetDir(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetHostname(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetAuthKey(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetControlURL(
sd: ::std::os::raw::c_int,
str_: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetEphemeral(sd: ::std::os::raw::c_int, e: GoInt) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetSetLogFD(
sd: ::std::os::raw::c_int,
fd: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetLoopback(
sd: ::std::os::raw::c_int,
addrOut: *mut ::std::os::raw::c_char,
addrLen: usize,
proxyOut: *mut ::std::os::raw::c_char,
localOut: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn TsnetEnableFunnelToLocalhostPlaintextHttp1(
sd: ::std::os::raw::c_int,
localhostPort: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}

View File

@ -0,0 +1,350 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use std::ffi::{CStr, CString};
use std::io::{Read, Write};
#[cfg(not(target_os = "windows"))]
use std::os::fd::{AsRawFd, RawFd};
#[cfg(target_os = "windows")]
use std::os::windows::io::{AsRawHandle, RawHandle};
use std::os::raw::{c_char, c_int};
mod bindings;
#[cfg(test)]
mod test;
type GoInt = i64;
use bindings::*;
use libc;
#[derive(Debug)]
pub enum TailscaleError {
ApiError(c_int, String),
BadFileDescriptor,
BufferTooSmall,
NulError(std::ffi::NulError),
InvalidUtf8(std::str::Utf8Error),
IoError(std::io::Error),
InvalidHandle,
}
impl From<std::ffi::NulError> for TailscaleError {
fn from(err: std::ffi::NulError) -> Self {
TailscaleError::NulError(err)
}
}
impl From<std::str::Utf8Error> for TailscaleError {
fn from(err: std::str::Utf8Error) -> Self {
TailscaleError::InvalidUtf8(err)
}
}
impl From<std::io::Error> for TailscaleError {
fn from(err: std::io::Error) -> Self {
TailscaleError::IoError(err)
}
}
// Helper function to get error message from the server handle
// This helper is needed because TsnetErrmsg requires the server handle (sd)
fn get_tsnet_errmsg(sd: c_int) -> String {
let mut buf = [0u8; 256]; // Choose a reasonable buffer size
let message = unsafe { TsnetErrmsg(sd, buf.as_mut_ptr() as *mut c_char, buf.len()) };
if message == 0 {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
c_str.to_string_lossy().into_owned()
} else {
format!(
"(Failed to get error message, TsnetErrmsg returned {})",
message
)
}
}
fn parse_tsnet_result(sd: c_int, ret: c_int) -> Result<(), TailscaleError> {
match ret {
0 => Ok(()),
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
_ => {
let message = get_tsnet_errmsg(sd);
Err(TailscaleError::ApiError(ret, message))
}
}
}
pub struct Tailscale(c_int);
// A TailscaleListener is a socket on the tailnet listening for connections.
//
// It is much like allocating a system socket(2) and calling listen(2).
// Accept connections with tailscale_accept and close the listener with close.
//
// Under the hood, a tailscale_listener is one half of a socketpair itself,
// used to move the connection fd from Go to C. This means you can use epoll
// or its equivalent on a tailscale_listener to know if there is a connection
// read to accept.
pub struct TailscaleListener(c_int);
// A TailscaleConn is a connection to an address on the tailnet.
//
// It is a pipe(2) on which you can use read(2), write(2), and close(2).
// For extra control over the connection, see the tailscale_conn_* functions.
pub struct TailscaleConn(c_int);
// NEEDS REVIEW. CANNOT BE BADLY DONE
impl Drop for Tailscale {
fn drop(&mut self) {
let ret = unsafe { TsnetClose(self.0) };
if ret != 0 && ret != libc::EBADF {
eprintln!("Error closing Tailscale server {}: {}", self.0, ret);
}
}
}
impl Drop for TailscaleListener {
fn drop(&mut self) {
// TailscaleListener is treated like a file descriptor.
let ret = unsafe { libc::close(self.0) };
if ret != 0 && ret != libc::EBADF {
eprintln!("Error closing Tailscale listener {}: {}", self.0, ret);
}
}
}
impl Drop for TailscaleConn {
fn drop(&mut self) {
// TailscaleConn is treated like a file descriptor (pipe).
let ret = unsafe { libc::close(self.0) };
if ret != 0 && ret != libc::EBADF {
eprintln!("Error closing Tailscale connection {}: {}", self.0, ret);
}
}
}
impl Tailscale {
pub fn new() -> Self {
Tailscale(unsafe { TsnetNewServer() })
}
pub fn start(&self) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetStart(self.0) };
parse_tsnet_result(self.0, ret)
}
pub fn up(&self) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetUp(self.0) };
parse_tsnet_result(self.0, ret)
}
pub fn close(&self) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetClose(self.0) };
parse_tsnet_result(self.0, ret)
}
pub fn set_dir(&self, dir: &str) -> Result<(), TailscaleError> {
let c_dir = CString::new(dir)?;
let ret = unsafe { TsnetSetDir(self.0, c_dir.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_hostname<T: AsRef<str>>(&self, hostname: T) -> Result<(), TailscaleError> {
let c_hostname = CString::new(hostname.as_ref())?;
let ret = unsafe { TsnetSetHostname(self.0, c_hostname.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_authkey(&self, authkey: &str) -> Result<(), TailscaleError> {
let c_authkey = CString::new(authkey)?;
let ret = unsafe { TsnetSetAuthKey(self.0, c_authkey.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_control_url(&self, control_url: &str) -> Result<(), TailscaleError> {
let c_control_url = CString::new(control_url)?;
let ret = unsafe { TsnetSetControlURL(self.0, c_control_url.as_ptr() as *mut c_char) };
parse_tsnet_result(self.0, ret)
}
pub fn set_ephemeral(&self, ephemeral: bool) -> Result<(), TailscaleError> {
let e: GoInt = if ephemeral { 1 } else { 0 };
// Use GoInt (i64) based on bindgen output
let ret = unsafe { TsnetSetEphemeral(self.0, e) };
parse_tsnet_result(self.0, ret)
}
pub fn set_log_fd(&self, fd: i32) -> Result<(), TailscaleError> {
let ret = unsafe { TsnetSetLogFD(self.0, fd) };
parse_tsnet_result(self.0, ret)
}
pub fn get_ips<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str, TailscaleError> {
let ret = unsafe { TsnetGetIps(self.0, buf.as_mut_ptr() as *mut c_char, buf.len()) };
match ret {
0 => {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
c_str.to_str().map_err(TailscaleError::from) // Convert Utf8Error
}
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
_ => {
let err_msg = get_tsnet_errmsg(self.0);
Err(TailscaleError::ApiError(ret, err_msg))
}
}
}
pub fn loopback(
&self,
addr_buf: &mut [u8],
proxy_buf: &mut [u8],
local_buf: &mut [u8],
) -> Result<(), TailscaleError> {
// C header says proxy_out and local_out must hold 33 bytes.
if proxy_buf.len() < 33 || local_buf.len() < 33 {
return Err(TailscaleError::BufferTooSmall); // Custom check based on docs
}
let ret = unsafe {
TsnetLoopback(
self.0,
addr_buf.as_mut_ptr() as *mut c_char,
addr_buf.len(),
proxy_buf.as_mut_ptr() as *mut c_char,
local_buf.as_mut_ptr() as *mut c_char,
)
};
parse_tsnet_result(self.0, ret)
}
pub fn dial(&self, network: &str, addr: &str) -> Result<TailscaleConn, TailscaleError> {
let c_network = CString::new(network)?;
let c_addr = CString::new(addr)?;
let mut conn_out: c_int = -1;
let ret = unsafe {
TsnetDial(
self.0,
c_network.as_ptr() as *mut c_char,
c_addr.as_ptr() as *mut c_char,
&mut conn_out,
)
};
parse_tsnet_result(self.0, ret)?;
if ret == 0 && conn_out != -1 {
Ok(TailscaleConn(conn_out))
} else if ret == 0 {
Err(TailscaleError::InvalidHandle)
} else {
unreachable!();
}
}
pub fn listen(&self, network: &str, addr: &str) -> Result<TailscaleListener, TailscaleError> {
let c_network = CString::new(network)?;
let c_addr = CString::new(addr)?;
let mut listener_out: c_int = -1; // Use c_int for the output pointer
let ret = unsafe {
TsnetListen(
self.0,
c_network.as_ptr() as *mut c_char,
c_addr.as_ptr() as *mut c_char,
&mut listener_out,
)
};
parse_tsnet_result(self.0, ret)?;
if ret == 0 && listener_out != -1 {
Ok(TailscaleListener(listener_out))
} else if ret == 0 {
Err(TailscaleError::InvalidHandle)
} else {
unreachable!();
}
}
pub fn enable_funnel_to_localhost_plaintext_http1(
&self,
localhost_port: i32,
) -> Result<(), TailscaleError> {
let ret =
unsafe { TsnetEnableFunnelToLocalhostPlaintextHttp1(self.0, localhost_port as c_int) };
parse_tsnet_result(self.0, ret)
}
pub fn get_last_error_message<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str, TailscaleError> {
let ret = unsafe { TsnetErrmsg(self.0, buf.as_mut_ptr() as *mut c_char, buf.len()) };
match ret {
0 => {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
c_str.to_str().map_err(TailscaleError::from) // Convert Utf8Error
}
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
// TsnetErrmsg should ideally not return other codes, but handle defensively
_ => Err(TailscaleError::ApiError(
ret,
format!("TsnetErrmsg returned unknown code {}", ret),
)),
}
}
}
#[cfg(not(target_os = "windows"))]
// Requires the connection handle to behave like a raw file descriptor.
impl AsRawFd for TailscaleConn {
fn as_raw_fd(&self) -> RawFd {
self.0
}
}
#[cfg(target_os = "windows")]
impl AsRawHandle for TailscaleConn {
fn as_raw_handle(&self) -> RawHandle {
self.0
}
}
impl Read for TailscaleConn {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
#[cfg(not(target_os = "windows"))]
let fd = self.as_raw_fd();
#[cfg(target_os = "windows")]
let fd = self.as_raw_handle();
// Safety: Calling libc::read on a valid file descriptor.
// The caller must ensure the handle is valid for reading (it is after successful dial/accept).
let n = unsafe {
libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
}
impl Write for TailscaleConn {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let fd = self.as_raw_fd();
// Safety: Calling libc::write on a valid file descriptor.
// The caller must ensure the handle is valid for writing (it is after successful dial/accept).
let n = unsafe {
libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len())
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
fn flush(&mut self) -> std::io::Result<()> {
// For a pipe/socket, flush is often a no-op after write.
Ok(())
}
}

View File

@ -0,0 +1,30 @@
use std::error::Error;
use crate::{Tailscale, TailscaleError};
#[test]
fn start_listener() -> Result<(), TailscaleError> {
println!("Creating server");
// Create a new server
let ts = Tailscale::new();
// Configure it
println!("Configuring directory");
ts.set_dir("/tmp/tailscale-rust-test")?;
println!("Configuring hostname");
ts.set_hostname("my-rust-node")?;
println!("Setting ephemeral");
//ts.set_authkey("tskey-...")?; // Set authkey if needed for auto-registration
ts.set_ephemeral(true)?;
// Bring the server up
println!("Starting Tailscale...");
ts.up()?;
println!("Tailscale started!");
// Get IPs
let mut ip_buf = [0u8; 256];
let ips = ts.get_ips(&mut ip_buf)?;
println!("Tailscale IPs: {}", ips);
Ok(())
}

View File

@ -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.2.0-beta", "version": "0.3.0-rc-5",
"identifier": "dev.drop.app", "identifier": "dev.drop.app",
"build": { "build": {
"beforeDevCommand": "yarn dev --port 1432", "beforeDevCommand": "yarn dev --port 1432",
@ -11,7 +11,11 @@
}, },
"app": { "app": {
"security": { "security": {
"csp": null "csp": null,
"assetProtocol": {
"enable": true,
"scope": {}
}
} }
}, },
"plugins": { "plugins": {

View File

@ -17,7 +17,7 @@ export type User = {
username: string; username: string;
admin: boolean; admin: boolean;
displayName: string; displayName: string;
profilePicture: string; profilePictureObjectId: string;
}; };
export type AppState = { export type AppState = {
@ -30,11 +30,11 @@ export type Game = {
mName: string; mName: string;
mShortDescription: string; mShortDescription: string;
mDescription: string; mDescription: string;
mIconId: string; mIconObjectId: string;
mBannerId: string; mBannerObjectId: string;
mCoverId: string; mCoverObjectId: string;
mImageLibrary: string[]; mImageLibraryObjectIds: string[];
mImageCarousel: string[]; mImageCarouselObjectIds: string[];
}; };
export type GameVersion = { export type GameVersion = {
@ -59,11 +59,13 @@ 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 {

4919
yarn.lock

File diff suppressed because it is too large Load Diff