Compare commits

..

3 Commits

Author SHA1 Message Date
6dedf39420 bump version and update ci 2025-07-18 19:57:37 +10:00
79373cb0c1 fix: clippy 2025-07-18 19:38:33 +10:00
f264600619 fix: reverse proxy 400 due to duplicate header 2025-07-18 19:36:10 +10:00
72 changed files with 918 additions and 1791 deletions

View File

@ -25,7 +25,7 @@ jobs:
- platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
args: ''
- platform: 'ubuntu-22.04-arm'
args: ''
args: '--target aarch64-unknown-linux-gnu'
- platform: 'windows-latest'
args: ''
@ -40,7 +40,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'yarn'
- name: install Rust nightly
uses: dtolnay/rust-toolchain@nightly
@ -52,74 +51,19 @@ jobs:
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf binutils fuse
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-latest'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
curl https://droposs.org/drop.crt --output drop.pem
sudo security authorizationdb write com.apple.trust-settings.admin allow
sudo security add-trusted-cert -d -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
sudo security authorizationdb remove com.apple.trust-settings.admin
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: Verify Certificate
if: matrix.platform == 'macos-latest'
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Drop OSS")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported. Using identity: $CERT_ID"
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: install frontend dependencies
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
- id: build
uses: tauri-apps/tauri-action@v0
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
tagName: v__VERSION__ # update the below "upload artifacts to GitHub" if this gets updated
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 }}
- name: Bundle AppImage
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
run: |
./build_appimage.sh --nobuild
- name: Upload binaries to release
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: build/appimage/*.AppImage
file_glob: true
release_id: ${{ steps.build.outputs.releaseId }}
overwrite: true

11
.gitignore vendored
View File

@ -26,13 +26,4 @@ dist-ssr
.output
src-tauri/flamegraph.svg
src-tauri/perf*
build/appimage/*
!build/appimage/drop-app.d
build/appimage/drop-app.d/usr/bin/*
!build/appimage/drop-app.d/usr/bin/.gitkeep
build/appimage/drop-app.d/usr/lib/*
!build/appimage/drop-app.d/usr/lib/.gitkeep
src-tauri/perf*

View File

@ -1,5 +1,4 @@
<template>
<LoadingIndicator />
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
@ -27,7 +26,6 @@ try {
console.error("failed to parse state", e);
}
// This is inefficient but apparently we do it lol
router.beforeEach(async () => {
try {
state.value = JSON.parse(await invoke("fetch_state"));

View File

@ -1 +0,0 @@
drop-oss-app.png

View File

@ -1,8 +0,0 @@
#!/bin/sh
program="drop-app"
# point to libraries and run
LD_LIBRARY_PATH=$APPDIR/usr/lib:/usr/local/lib:/usr/lib $APPDIR/usr/bin/$program $@
# vim:ft=sh

View File

@ -1,8 +0,0 @@
[Desktop Entry]
Name=Drop Desktop App
Comment=The client application for the open-source, self-hosted game distribution platform Drop.
Exec=AppRun
Icon=drop-oss-app
Type=Application
Categories=Game;Network
MimeType=x-scheme-handler/drop

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,42 +0,0 @@
#!/bin/sh
set -e
# run this from the root of the git repo to make this work
arch="$(uname -m)"
git_dir="$PWD"
target_dir="$git_dir/src-tauri/target"
appimage_dir="$git_dir/build/appimage"
appdir="$appimage_dir/drop-app.d"
build() {
# set up the repo
git submodule init
git submodule update --recursive
# set up yarn and build
yarn
yarn tauri build
}
rm -f $appdir/usr/bin/* $appdir/usr/lib/*
if [[ ! "$1" == "--nobuild" ]]; then
build
fi
# install binaries in the appdir, then the libraries
cp $target_dir/release/drop-app $appdir/usr/bin
for lib_name in $(ldd "$target_dir/release/drop-app" | grep '=>' | awk '{ print $(NF-1) }');
do
echo $lib_name
sudo install -g 1000 -o 1000 -Dm755 "$(ls -L1 $lib_name)" $appdir/usr/lib
done
wget -O $appimage_dir/appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$arch.AppImage
cd $appimage_dir
chmod u+x appimagetool
./appimagetool $appdir
ls "$appimage_dir/"

View File

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

View File

@ -23,7 +23,7 @@
<MenuItems
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
>
<div class="flex-col gap-y-2">
<PanelWidget class="flex-col gap-y-2">
<NuxtLink
to="/id/me"
class="transition inline-flex items-center w-full py-3 px-4 hover:bg-zinc-800"
@ -65,7 +65,7 @@
</button>
</MenuItem>
</div>
</div>
</PanelWidget>
</MenuItems>
</transition>
</Menu>

View File

@ -13,7 +13,11 @@
<div class="max-w-lg">
<slot />
<div class="mt-10">
<div>
<button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="text-sm text-left font-semibold leading-7 text-blue-600"
>
<div v-if="loading" role="status">
<svg
aria-hidden="true"
@ -33,19 +37,10 @@
</svg>
<span class="sr-only">Loading...</span>
</div>
<span class="inline-flex gap-x-8 items-center" v-else>
<button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
>
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
</button>
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
Use a code &rarr;
</NuxtLink>
<span v-else>
Sign in with your browser <span aria-hidden="true">&rarr;</span>
</span>
</div>
</button>
<div class="mt-5" v-if="offerManual">
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
@ -126,7 +121,6 @@
<script setup lang="ts">
import { XCircleIcon } from "@heroicons/vue/16/solid";
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
const loading = ref(false);

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<div
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
>
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
<Wordmark class="mt-1" />

View File

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

View File

@ -18,7 +18,7 @@ export function setupHooks() {
});
listen("auth/finished", async (event) => {
router.push("/library");
router.push("/store");
state.value = JSON.parse(await invoke("fetch_state"));
});
@ -30,31 +30,12 @@ export function setupHooks() {
description: `Drop encountered an error while downloading your game: "${(
event.payload as unknown as string
).toString()}"`,
buttonText: "Close",
buttonText: "Close"
},
(e, c) => c()
);
});
// This is for errors that (we think) aren't our fault
listen("launch_external_error", (event) => {
createModal(
ModalType.Confirmation,
{
title: "Did something go wrong?",
description:
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
buttonText: "Open",
},
async (e, c) => {
if (e == "confirm") {
await invoke("open_process_logs", { gameId: event.payload });
}
c();
}
);
});
/*
document.addEventListener("contextmenu", (event) => {
@ -82,6 +63,6 @@ export function initialNavigation(state: Ref<AppState>) {
router.push("/error/serverunavailable");
break;
default:
router.push("/library");
router.push("/store");
}
}

View File

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

View File

@ -1,37 +0,0 @@
<template>
<div class="min-h-full w-full flex items-center justify-center">
<div class="flex flex-col items-center">
<div class="text-center">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Device authorization
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-md mx-auto">
Open Drop on another one of your devices, and use your account
dropdown to "Authorize client", and enter the code below.
</p>
<div
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
>
<span v-for="letter in code.split('')">{{ letter }}</span>
</div>
</div>
<div class="mt-10 flex items-center justify-center gap-x-6">
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
><span aria-hidden="true">&larr;</span> Use a different method
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
const code = await invoke<string>("auth_initiate_code");
definePageMeta({
layout: "mini",
});
</script>

View File

@ -76,7 +76,7 @@
<TransitionGroup name="slide" tag="div" class="h-full">
<img
v-for="(url, index) in mediaUrls"
:key="index"
:key="url"
:src="url"
class="absolute inset-0 w-full h-full object-cover"
v-show="index === currentImageIndex"
@ -157,8 +157,8 @@
<template #default>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<h3 class="text-base font-semibold text-zinc-100">
Install {{ game.mName }}?
<h3 class="text-base font-semibold text-zinc-100"
>Install {{ game.mName }}?
</h3>
<div class="mt-2">
<p class="text-sm text-zinc-400">
@ -350,7 +350,9 @@
<template #buttons>
<LoadingButton
@click="() => install()"
:disabled="!(versionOptions && versionOptions.length > 0)"
:disabled="
!(versionOptions && versionOptions.length > 0)
"
:loading="installLoading"
type="submit"
class="ml-2 w-full sm:w-fit"
@ -368,18 +370,7 @@
</template>
</ModalTemplate>
<!--
Dear future DecDuck,
This v-if is necessary for Vue rendering reasons
(it tries to access the game version for not installed games)
You have already tried to remove it
Don't.
-->
<GameOptionsModal
v-if="status.type === GameStatusEnum.Installed"
v-model="configureModalOpen"
:game-id="game.id"
/>
<GameOptionsModal v-if="status.type === GameStatusEnum.Installed" v-model="configureModalOpen" :game-id="game.id" />
<Transition
enter="transition ease-out duration-300"
@ -429,7 +420,7 @@
<img
v-for="(url, index) in mediaUrls"
v-show="currentImageIndex === index"
:key="index"
:key="url"
:src="url"
class="max-h-[90vh] max-w-[90vw] object-contain"
:alt="`${game.mName} screenshot ${index + 1}`"
@ -450,6 +441,10 @@
<script setup lang="ts">
import {
Dialog,
DialogTitle,
TransitionChild,
TransitionRoot,
Listbox,
ListboxButton,
ListboxLabel,
@ -487,10 +482,7 @@ const bannerUrl = await useObject(game.value.mBannerObjectId);
// Get all available images
const mediaUrls = await Promise.all(
game.value.mImageCarouselObjectIds.map(async (v) => {
const src = await useObject(v);
return src;
})
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
);
const htmlDescription = micromark(game.value.mDescription);
@ -504,6 +496,7 @@ const currentImageIndex = ref(0);
const configureModalOpen = ref(false);
async function installFlow() {
installFlowOpen.value = true;
versionOptions.value = undefined;
@ -542,11 +535,12 @@ async function install() {
}
async function resumeDownload() {
try {
await invoke("resume_download", { gameId: game.value.id });
} catch (e) {
console.error(e);
}
try {
await invoke("resume_download", { gameId: game.value.id })
}
catch(e) {
console.error(e)
}
}
async function launch() {

View File

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

View File

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

315
src-tauri/Cargo.lock generated
View File

@ -68,12 +68,6 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ashpd"
version = "0.11.0"
@ -345,22 +339,6 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "async-tungstenite"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886"
dependencies = [
"atomic-waker",
"futures-core",
"futures-io",
"futures-task",
"futures-util",
"log",
"pin-project-lite",
"tungstenite",
]
[[package]]
name = "atk"
version = "0.18.2"
@ -449,6 +427,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "binary-stream"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4ef03ef225ea9a0b680a5926a58cb45d8eb56abf23d8a8b5c5dbc61235e2dac"
dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "bincode"
version = "1.3.3"
@ -458,30 +445,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bitcode"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf300f4aa6e66f3bdff11f1236a88c622fe47ea814524792240b4d554d9858ee"
dependencies = [
"arrayvec",
"bitcode_derive",
"bytemuck",
"glam",
"serde",
]
[[package]]
name = "bitcode_derive"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b6b4cb608b8282dc3b53d0f4c9ab404655d562674c682db7e6c0458cc83c23"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -575,9 +538,9 @@ checksum = "66bb12751a83493ef4b8da1120451a262554e216a247f14b48cb5e8fe7ed8bdf"
[[package]]
name = "brotli"
version = "8.0.1"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -586,9 +549,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "5.0.0"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -983,15 +946,15 @@ dependencies = [
[[package]]
name = "cssparser"
version = "0.29.6"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa"
checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"itoa 0.4.8",
"matches",
"phf 0.10.1",
"phf 0.8.0",
"proc-macro2",
"quote",
"smallvec",
@ -1284,10 +1247,9 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.3.1-appimage"
version = "0.3.0-rc-7"
dependencies = [
"atomic-instant-full",
"bitcode",
"boxcar",
"cacache 13.1.0",
"chrono",
@ -1296,7 +1258,6 @@ dependencies = [
"droplet-rs",
"dynfmt",
"filetime",
"futures-lite",
"gethostname",
"hex 0.4.3",
"http 1.3.1",
@ -1306,7 +1267,6 @@ dependencies = [
"log4rs",
"md5",
"native_model",
"page_size",
"parking_lot 0.12.3",
"rand 0.9.1",
"rayon",
@ -1314,11 +1274,11 @@ dependencies = [
"reqwest 0.12.16",
"reqwest-middleware 0.4.2",
"reqwest-middleware-cache",
"reqwest-websocket",
"rustbreak",
"rustix 0.38.44",
"schemars",
"serde",
"serde-binary",
"serde_json",
"serde_with",
"sha1",
@ -1330,7 +1290,6 @@ dependencies = [
"tauri-plugin-autostart",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
@ -1947,12 +1906,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "glam"
version = "0.30.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
[[package]]
name = "glib"
version = "0.18.5"
@ -2178,14 +2131,16 @@ dependencies = [
[[package]]
name = "html5ever"
version = "0.29.1"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever",
"match_token",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
@ -2196,7 +2151,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
"itoa 1.0.15",
]
[[package]]
@ -2207,7 +2162,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
"itoa 1.0.15",
]
[[package]]
@ -2316,7 +2271,7 @@ dependencies = [
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"itoa 1.0.15",
"pin-project-lite",
"socket2",
"tokio",
@ -2338,7 +2293,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"httparse",
"itoa",
"itoa 1.0.15",
"pin-project-lite",
"smallvec",
"tokio",
@ -2608,6 +2563,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.15"
@ -2723,13 +2684,14 @@ dependencies = [
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
dependencies = [
"cssparser",
"html5ever",
"indexmap 2.9.0",
"indexmap 1.9.3",
"matches",
"selectors",
]
@ -2894,29 +2856,18 @@ dependencies = [
[[package]]
name = "markup5ever"
version = "0.14.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.11.3",
"phf_codegen 0.11.3",
"phf 0.10.1",
"phf_codegen 0.10.0",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "match_token"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "matches"
version = "0.1.10"
@ -3031,9 +2982,9 @@ dependencies = [
[[package]]
name = "muda"
version = "0.17.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988"
checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492"
dependencies = [
"crossbeam-channel",
"dpi",
@ -3524,16 +3475,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "pango"
version = "0.18.3"
@ -3647,7 +3588,9 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
[[package]]
@ -3656,9 +3599,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_macros 0.10.0",
"phf_shared 0.10.0",
"proc-macro-hack",
]
[[package]]
@ -3683,12 +3624,12 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.11.3"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]]
@ -3723,12 +3664,12 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.10.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
"proc-macro2",
"quote",
@ -4386,24 +4327,6 @@ dependencies = [
"url",
]
[[package]]
name = "reqwest-websocket"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f91a811daaa8b54faeaec9d507a336897a3d243834a4965254a17d39da8b5c9"
dependencies = [
"async-tungstenite",
"bytes",
"futures-util",
"reqwest 0.12.16",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
"tungstenite",
"web-sys",
]
[[package]]
name = "rfd"
version = "0.15.3"
@ -4635,20 +4558,22 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "selectors"
version = "0.24.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
dependencies = [
"bitflags 1.3.2",
"cssparser",
"derive_more",
"fxhash",
"log",
"matches",
"phf 0.8.0",
"phf_codegen 0.8.0",
"precomputed-hash",
"servo_arc",
"smallvec",
"thin-slice",
]
[[package]]
@ -4669,6 +4594,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-binary"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b550db407b83ed53a4f76f888bfd7441b685abc2c086e20fb47781a286940506"
dependencies = [
"binary-stream",
"serde",
"thiserror 1.0.69",
]
[[package]]
name = "serde-untagged"
version = "0.1.7"
@ -4718,7 +4654,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"itoa 1.0.15",
"memchr",
"ryu",
"serde",
@ -4751,7 +4687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"itoa 1.0.15",
"ryu",
"serde",
]
@ -4793,7 +4729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.9.0",
"itoa",
"itoa 1.0.15",
"ryu",
"serde",
"unsafe-libyaml",
@ -4823,9 +4759,9 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.2.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741"
checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
dependencies = [
"nodrop",
"stable_deref_trait",
@ -5226,9 +5162,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.34.0"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.1",
@ -5302,16 +5238,17 @@ dependencies = [
[[package]]
name = "tauri"
version = "2.6.2"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "124e129c9c0faa6bec792c5948c89e86c90094133b0b9044df0ce5f0a8efaa0d"
checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc"
dependencies = [
"anyhow",
"bytes",
"dirs 6.0.0",
"dunce",
"embed_plist",
"getrandom 0.3.3",
"futures-util",
"getrandom 0.2.16",
"glob",
"gtk",
"heck 0.5.0",
@ -5353,9 +5290,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f025c389d3adb83114bec704da973142e82fc6ec799c7c750c5e21cefaec83"
checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc"
dependencies = [
"anyhow",
"cargo_toml",
@ -5375,9 +5312,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5df493a1075a241065bc865ed5ef8d0fbc1e76c7afdc0bf0eccfaa7d4f0e406"
checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47"
dependencies = [
"base64 0.22.1",
"brotli",
@ -5402,9 +5339,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.3.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f237fbea5866fa5f2a60a21bea807a2d6e0379db070d89c3a10ac0f2d4649bbc"
checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -5416,9 +5353,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9a0bd00bf1930ad1a604d08b0eb6b2a9c1822686d65d7f4731a7723b8901d3"
checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601"
dependencies = [
"anyhow",
"glob",
@ -5505,28 +5442,6 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation 0.3.1",
"open",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"url",
"windows",
"zbus",
]
[[package]]
name = "tauri-plugin-os"
version = "2.2.1"
@ -5584,9 +5499,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.7.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e7bb73d1bceac06c20b3f755b2c8a2cb13b20b50083084a8cf3700daf397ba4"
checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39"
dependencies = [
"cookie",
"dpi",
@ -5606,9 +5521,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.7.1"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902b5aa9035e16f342eb64f8bf06ccdc2808e411a2525ed1d07672fa4e780bad"
checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2"
dependencies = [
"gtk",
"http 1.3.1",
@ -5633,9 +5548,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.5.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41743bbbeb96c3a100d234e5a0b60a46d5aa068f266160862c7afdbf828ca02e"
checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4"
dependencies = [
"anyhow",
"brotli",
@ -5704,6 +5619,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "thin-slice"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.69"
@ -5773,7 +5694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"itoa 1.0.15",
"num-conv",
"powerfmt",
"serde",
@ -5878,7 +5799,6 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@ -6025,9 +5945,9 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.21.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a"
checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0"
dependencies = [
"crossbeam-channel",
"dirs 6.0.0",
@ -6057,23 +5977,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.9.1",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.3"
@ -6535,9 +6438,9 @@ dependencies = [
[[package]]
name = "webview2-com"
version = "0.38.0"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
@ -6560,9 +6463,9 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
version = "0.38.0"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295"
dependencies = [
"thiserror 2.0.12",
"windows",
@ -7035,9 +6938,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wry"
version = "0.52.1"
version = "0.51.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2"
dependencies = [
"base64 0.22.1",
"block2 0.6.1",

View File

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

View File

@ -1,3 +1,3 @@
fn main() {
tauri_build::build();
tauri_build::build()
}

View File

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

View File

@ -13,8 +13,8 @@ pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mut
let download_manager = state.lock().unwrap().download_manager.clone();
match download_manager.ensure_terminated() {
Ok(res) => match res {
Ok(()) => debug!("download manager terminated correctly"),
Err(()) => error!("download manager failed to terminate correctly"),
Ok(_) => debug!("download manager terminated correctly"),
Err(_) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{e:?}"),
}

View File

@ -7,7 +7,7 @@ use std::{
use serde_json::Value;
use crate::{
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
};
use super::{
@ -59,8 +59,6 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
lock.applications.install_dirs.push(new_dir);
drop(lock);
scan_install_dirs();
Ok(())
}

View File

@ -10,7 +10,7 @@ use chrono::Utc;
use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{Serialize, de::DeserializeOwned};
use serde::{de::DeserializeOwned, Serialize};
use url::Url;
use crate::DB;
@ -67,18 +67,20 @@ impl DatabaseImpls for DatabaseInterface {
let exists = fs::exists(db_path.clone()).unwrap();
if exists {
match PathDatabase::load_from_path(db_path.clone()) {
match exists {
true => match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
},
false => {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
}
} else {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
}
}
@ -122,7 +124,14 @@ fn handle_invalid_database(
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = Database;
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
@ -133,21 +142,14 @@ impl<'a> DerefMut for DBWrite<'a> {
&mut self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for DBWrite<'_> {
impl<'a> Drop for DBWrite<'a> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(()) => {}
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {e}");
panic!("database failed to save with error {e}")
@ -155,7 +157,6 @@ impl Drop for DBWrite<'_> {
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => DBRead(data),

View File

@ -2,4 +2,3 @@ pub mod commands;
pub mod db;
pub mod debug;
pub mod models;
pub mod scan;

View File

@ -1,14 +1,8 @@
/**
* NEXT BREAKING CHANGE
*
* UPDATE DATABASE TO USE RPMSERDENAMED
*
* WE CAN'T DELETE ANY FIELDS
*/
use crate::database::models::data::Database;
pub mod data {
use std::path::PathBuf;
use native_model::native_model;
use serde::{Deserialize, Serialize};
@ -24,14 +18,16 @@ pub mod data {
pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::collections::HashMap;
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::{Deserialize, Serialize, native_model};
use super::*;
fn default_template() -> String {
"{}".to_owned()
@ -119,7 +115,6 @@ pub mod data {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Validating { version_name: String },
Running {},
}
@ -179,10 +174,7 @@ pub mod data {
use serde_with::serde_as;
use super::{
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
GameVersion, Serialize, Settings, native_model, v1,
};
use super::*;
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
@ -214,7 +206,7 @@ pub mod data {
applications: value.applications,
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: None,
compat_info: crate::database::models::Database::create_new_compat_info(),
}
}
}
@ -291,10 +283,7 @@ pub mod data {
mod v3 {
use std::path::PathBuf;
use super::{
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
Settings, native_model, v2,
};
use super::*;
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
@ -308,7 +297,6 @@ pub mod data {
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
impl From<v2::Database> for Database {
fn from(value: v2::Database) -> Self {
Self {
@ -318,13 +306,22 @@ pub mod data {
applications: value.applications.into(),
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: None,
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>,
@ -339,11 +336,11 @@ pub mod data {
transient_statuses: HashMap::new(),
},
prev_database,
base_url: String::new(),
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: None,
compat_info: Database::create_new_compat_info(),
}
}
}

View File

@ -1,52 +0,0 @@
use std::fs;
use log::warn;
use crate::{
database::{
db::borrow_db_mut_checked,
models::data::v1::{DownloadType, DownloadableMetadata},
},
games::{
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
library::set_partially_installed_db,
},
};
pub fn scan_install_dirs() {
let mut db_lock = borrow_db_mut_checked();
for install_dir in db_lock.applications.install_dirs.clone() {
let Ok(files) = fs::read_dir(install_dir) else {
continue;
};
for game in files.into_iter().flatten() {
let drop_data_file = game.path().join(DROP_DATA_PATH);
if !drop_data_file.exists() {
continue;
}
let game_id = game.file_name().into_string().unwrap();
let Ok(drop_data) = DropData::read(&game.path()) else {
warn!(
".dropdata exists for {}, but couldn't read it. is it corrupted?",
game.file_name().into_string().unwrap()
);
continue;
};
if db_lock.applications.game_statuses.contains_key(&game_id) {
continue;
}
let metadata = DownloadableMetadata::new(
drop_data.game_id,
Some(drop_data.game_version),
DownloadType::Game,
);
set_partially_installed_db(
&mut db_lock,
&metadata,
drop_data.base_path.to_str().unwrap().to_string(),
None,
);
}
}
}

View File

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

View File

@ -1,10 +1,10 @@
use std::{
collections::HashMap,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex,
mpsc::{Receiver, Sender, channel},
},
thread::{JoinHandle, spawn},
thread::{spawn, JoinHandle},
};
use log::{debug, error, info, warn};
@ -176,7 +176,7 @@ impl DownloadManagerBuilder {
DownloadManagerSignal::Cancel(meta) => {
self.manage_cancel_signal(&meta);
}
}
};
}
}
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
@ -212,13 +212,13 @@ impl DownloadManagerBuilder {
if self.current_download_agent.is_some()
&& self.download_queue.read().front().unwrap()
== &self.current_download_agent.as_ref().unwrap().metadata()
{
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
{
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
debug!("current download queue: {:?}", self.download_queue.read());
@ -242,47 +242,43 @@ impl DownloadManagerBuilder {
let app_handle = self.app_handle.clone();
*download_thread_lock = Some(spawn(move || {
loop {
let download_result = match download_agent.download(&app_handle) {
// Ok(true) is for completed and exited properly
Ok(v) => v,
Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
return;
match download_agent.download(&app_handle) {
// Ok(true) is for completed and exited properly
Ok(true) => {
debug!("download {:?} has completed", download_agent.metadata());
match download_agent.validate() {
Ok(true) => {
download_agent.on_complete(&app_handle);
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
}
Ok(false) => {
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!(
"download {:?} has validation error {}",
download_agent.metadata(),
&e
);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
};
// If the download gets cancelled
// immediately return, on_cancelled gets called for us earlier
if !download_result {
return;
}
let validate_result = match download_agent.validate(&app_handle) {
Ok(v) => v,
Err(e) => {
error!(
"download {:?} has validation error {}",
download_agent.metadata(),
&e
);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
return;
}
};
if validate_result {
download_agent.on_complete(&app_handle);
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
return;
// Ok(false) is for incomplete but exited properly
Ok(false) => {
debug!("Donwload agent finished incomplete");
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, &e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
}));
self.set_status(DownloadManagerStatus::Downloading);
@ -299,12 +295,11 @@ impl DownloadManagerBuilder {
}
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
debug!("got signal Completed");
if let Some(interface) = &self.current_download_agent
&& interface.metadata() == meta
{
self.remove_and_cleanup_front_download(&meta);
if let Some(interface) = &self.current_download_agent {
if interface.metadata() == meta {
self.remove_and_cleanup_front_download(&meta);
}
}
self.push_ui_queue_update();
self.sender.send(DownloadManagerSignal::Go).unwrap();
}

View File

@ -23,13 +23,13 @@ use super::{
};
pub enum DownloadManagerSignal {
/// Resumes (or starts) the `DownloadManager`
/// Resumes (or starts) the DownloadManager
Go,
/// Pauses the `DownloadManager`
/// Pauses the DownloadManager
Stop,
/// Called when a `DownloadAgent` has fully completed a download.
/// Called when a DownloadAgent has fully completed a download.
Completed(DownloadableMetadata),
/// Generates and appends a `DownloadAgent`
/// Generates and appends a DownloadAgent
/// to the registry and queue
Queue(DownloadAgent),
/// Tells the Manager to stop the current
@ -70,14 +70,14 @@ pub enum DownloadStatus {
Error,
}
/// Accessible front-end for the `DownloadManager`
/// Accessible front-end for the DownloadManager
///
/// The system works entirely through signals, both internally and externally,
/// all of which are accessible through the `DownloadManagerSignal` type, but
/// all of which are accessible through the DownloadManagerSignal type, but
/// should not be used directly. Rather, signals are abstracted through this
/// interface.
///
/// The actual download queue may be accessed through the .`edit()` function,
/// The actual download queue may be accessed through the .edit() function,
/// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager {
@ -139,7 +139,7 @@ impl DownloadManager {
pub fn rearrange(&self, current_index: usize, new_index: usize) {
if current_index == new_index {
return;
}
};
let needs_pause = current_index == 0 || new_index == 0;
if needs_pause {
@ -148,7 +148,9 @@ impl DownloadManager {
.unwrap();
}
debug!("moving download at index {current_index} to index {new_index}");
debug!(
"moving download at index {current_index} to index {new_index}"
);
let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap();
@ -183,7 +185,7 @@ impl DownloadManager {
}
}
/// Takes in the locked value from .`edit()` and attempts to
/// Takes in the locked value from .edit() and attempts to
/// get the index of whatever id is passed in
fn get_index_from_id(
queue: &mut MutexGuard<'_, VecDeque<DownloadableMetadata>>,

View File

@ -14,14 +14,14 @@ use super::{
pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl;
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle);
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
fn on_complete(&self, app_handle: &AppHandle);
fn on_incomplete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle);
}

View File

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

View File

@ -22,7 +22,10 @@ impl From<DownloadThreadControlFlag> for bool {
/// false => Stop
impl From<bool> for DownloadThreadControlFlag {
fn from(value: bool) -> Self {
if value { DownloadThreadControlFlag::Go } else { DownloadThreadControlFlag::Stop }
match value {
true => DownloadThreadControlFlag::Go,
false => DownloadThreadControlFlag::Stop,
}
}
}
@ -38,9 +41,9 @@ impl DownloadThreadControl {
}
}
pub fn get(&self) -> DownloadThreadControlFlag {
self.inner.load(Ordering::Acquire).into()
self.inner.load(Ordering::Relaxed).into()
}
pub fn set(&self, flag: DownloadThreadControlFlag) {
self.inner.store(flag.into(), Ordering::Release);
self.inner.store(flag.into(), Ordering::Relaxed);
}
}

View File

@ -39,20 +39,20 @@ impl ProgressHandle {
}
}
pub fn set(&self, amount: usize) {
self.progress.store(amount, Ordering::Release);
self.progress.store(amount, Ordering::Relaxed);
}
pub fn add(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::AcqRel);
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
calculate_update(&self.progress_object);
}
pub fn skip(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Acquire);
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
// Offset the bytes at last offset by this amount
self.progress_object
.bytes_last_update
.fetch_add(amount, Ordering::Acquire);
.fetch_add(amount, Ordering::Relaxed);
// Dont' fire update
}
}
@ -60,6 +60,7 @@ impl ProgressHandle {
impl ProgressObject {
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
// TODO: consolidate this calculation with the set_max function below
Self {
max: Arc::new(Mutex::new(max)),
progress_instances: Arc::new(arr),
@ -80,18 +81,19 @@ impl ProgressObject {
.lock()
.unwrap()
.iter()
.map(|instance| instance.load(Ordering::Acquire))
.map(|instance| instance.load(Ordering::Relaxed))
.sum()
}
pub fn reset(&self) {
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::SeqCst));
.for_each(|x| x.store(0, Ordering::Release));
}
pub fn get_max(&self) -> usize {
*self.max.lock().unwrap()
@ -125,7 +127,7 @@ pub fn calculate_update(progress: &ProgressObject) {
let max = progress.get_max();
let bytes_at_last_update = progress
.bytes_last_update
.swap(current_bytes_downloaded, Ordering::Acquire);
.swap(current_bytes_downloaded, Ordering::Relaxed);
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;

View File

@ -11,7 +11,7 @@ pub struct RollingProgressWindow<const S: usize> {
impl<const S: usize> RollingProgressWindow<S> {
pub fn new() -> Self {
Self {
window: Arc::new([(); S].map(|()| AtomicUsize::new(0))),
window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
current: Arc::new(AtomicUsize::new(0)),
}
}
@ -26,7 +26,7 @@ impl<const S: usize> RollingProgressWindow<S> {
.iter()
.enumerate()
.filter(|(i, _)| i < &current)
.map(|(_, x)| x.load(Ordering::Acquire))
.map(|(_, x)| x.load(Ordering::Relaxed))
.sum::<usize>()
/ S
}

View File

@ -5,7 +5,7 @@ use std::{
use serde_with::SerializeDisplay;
use super::remote_access_error::RemoteAccessError;
use super::{remote_access_error::RemoteAccessError};
// TODO: Rename / separate from downloads
#[derive(Debug, SerializeDisplay)]

View File

@ -4,28 +4,30 @@ use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum ProcessError {
SetupRequired,
NotInstalled,
AlreadyRunning,
NotDownloaded,
InvalidID,
InvalidVersion,
IOError(Error),
FormatError(String), // String errors supremacy
InvalidPlatform,
OpenerError(tauri_plugin_opener::Error)
}
impl Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ProcessError::SetupRequired => "Game not set up",
ProcessError::NotInstalled => "Game not installed",
ProcessError::AlreadyRunning => "Game already running",
ProcessError::InvalidID => "Invalid game ID",
ProcessError::InvalidVersion => "Invalid game version",
ProcessError::NotDownloaded => "Game not downloaded",
ProcessError::InvalidID => "Invalid Game ID",
ProcessError::InvalidVersion => "Invalid Game version",
ProcessError::IOError(error) => &error.to_string(),
ProcessError::InvalidPlatform => "This game cannot be played on the current platform",
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
ProcessError::FormatError(e) => &format!("Failed to format template: {e}"),
ProcessError::OpenerError(error) => &format!("Failed to open directory: {error}"),
};
};
write!(f, "{s}")
}
}

View File

@ -13,7 +13,6 @@ use super::drop_server_error::DropServerError;
#[derive(Debug, SerializeDisplay)]
pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>),
FetchErrorWS(Arc<reqwest_websocket::Error>),
ParsingError(ParseError),
InvalidEndpoint,
HandshakeFailed(String),
@ -22,7 +21,7 @@ pub enum RemoteAccessError {
UnparseableResponse(String),
ManifestDownloadFailed(StatusCode, String),
OutOfSync,
Cache(std::io::Error),
Cache(cacache::Error),
}
impl Display for RemoteAccessError {
@ -30,10 +29,7 @@ impl Display for RemoteAccessError {
match self {
RemoteAccessError::FetchError(error) => {
if error.is_connect() {
return write!(
f,
"Failed to connect to Drop server. Check if you access Drop through a browser, and then try again."
);
return write!(f, "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again.");
}
write!(
@ -42,44 +38,24 @@ impl Display for RemoteAccessError {
error,
error
.source()
.map(std::string::ToString::to_string)
.map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string()))
.unwrap()
)
}
RemoteAccessError::FetchErrorWS(error) => write!(
f,
"{}: {}",
error,
error
.source()
.map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string()))
.unwrap()
),
},
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{parse_error}")
}
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => {
write!(f, "failed to complete handshake: {message}")
}
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {message}"),
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {id}"),
RemoteAccessError::InvalidResponse(error) => write!(
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {}, {}", error.status_code, error.status_message),
RemoteAccessError::UnparseableResponse(error) => write!(f, "server returned an invalid response: {error}"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
f,
"server returned an invalid response: {}, {}",
error.status_code, error.status_message
),
RemoteAccessError::UnparseableResponse(error) => {
write!(f, "server returned an invalid response: {error}")
}
RemoteAccessError::ManifestDownloadFailed(status, response) => {
write!(f, "failed to download game manifest: {status} {response}")
}
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"
"failed to download game manifest: {status} {response}"
),
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {error}"),
}
}
@ -90,11 +66,6 @@ impl From<reqwest::Error> for RemoteAccessError {
RemoteAccessError::FetchError(Arc::new(err))
}
}
impl From<reqwest_websocket::Error> for RemoteAccessError {
fn from(err: reqwest_websocket::Error) -> Self {
RemoteAccessError::FetchErrorWS(Arc::new(err))
}
}
impl From<ParseError> for RemoteAccessError {
fn from(err: ParseError) -> Self {
RemoteAccessError::ParsingError(err)

View File

@ -1,18 +1,19 @@
use reqwest::blocking::Client;
use serde_json::json;
use url::Url;
use crate::{
DB,
database::db::DatabaseImpls,
error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC},
remote::{auth::generate_authorization_header, requests::make_request},
DB,
};
use super::collection::{Collection, Collections};
#[tauri::command]
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
@ -23,7 +24,7 @@ pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
#[tauri::command]
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let response = make_request(
&client,
&["/api/v1/client/collection/", &collection_id],
@ -37,7 +38,7 @@ pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAcces
#[tauri::command]
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let base_url = DB.fetch_base_url();
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
@ -56,7 +57,7 @@ pub fn add_game_to_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let url = Url::parse(&format!(
"{}api/v1/client/collection/{}/entry/",
DB.fetch_base_url(),
@ -73,7 +74,7 @@ pub fn add_game_to_collection(
#[tauri::command]
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let base_url = Url::parse(&format!(
"{}api/v1/client/collection/{}",
DB.fetch_base_url(),
@ -92,7 +93,7 @@ pub fn delete_game_in_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let base_url = Url::parse(&format!(
"{}api/v1/client/collection/{}/entry",
DB.fetch_base_url(),

View File

@ -3,23 +3,19 @@ use std::sync::Mutex;
use tauri::AppHandle;
use crate::{
AppState,
database::{
db::borrow_db_checked,
models::data::GameVersion,
},
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,
offline, AppState,
};
use super::{
library::{
FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic,
fetch_library_logic,
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
Game,
},
state::{GameStatusManager, GameStatusWithTransient},
};
@ -41,19 +37,19 @@ pub fn fetch_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
offline!(
let res = offline!(
state,
fetch_game_logic,
fetch_game_logic_offline,
game_id,
state
)
);
res
}
#[tauri::command]
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
let db_handle = borrow_db_checked();
GameStatusManager::fetch_state(&id, &db_handle)
GameStatusManager::fetch_state(&id)
}
#[tauri::command]

View File

@ -5,9 +5,7 @@ use std::{
use crate::{
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
download_manager::{
download_manager_frontend::DownloadManagerSignal, downloadable::Downloadable,
},
download_manager::{download_manager_frontend::DownloadManagerSignal, downloadable::Downloadable},
error::download_manager_error::DownloadManagerError,
AppState,
};

View File

@ -12,17 +12,13 @@ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObj
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::downloads::validate::validate_game_chunk;
use crate::games::library::{
on_game_complete, push_game_update, set_partially_installed,
};
use crate::games::state::GameStatusManager;
use crate::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::utils::DROP_CLIENT_SYNC;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
use std::collections::HashMap;
use std::fs::{OpenOptions, create_dir_all};
use std::fs::{create_dir_all, OpenOptions};
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
@ -30,7 +26,7 @@ use std::time::Instant;
use tauri::{AppHandle, Emitter};
#[cfg(target_os = "linux")]
use rustix::fs::{FallocateFlags, fallocate};
use rustix::fs::{fallocate, FallocateFlags};
use super::download_logic::download_game_chunk;
use super::drop_data::DropData;
@ -44,7 +40,7 @@ pub struct GameDownloadAgent {
pub manifest: Mutex<Option<DropManifest>>,
pub progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
pub dropdata: DropData,
pub stored_manifest: DropData,
status: Mutex<DownloadStatus>,
}
@ -85,46 +81,41 @@ impl GameDownloadAgent {
context_map: Mutex::new(HashMap::new()),
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender,
dropdata: stored_manifest,
stored_manifest,
status: Mutex::new(DownloadStatus::Queued),
}
}
// Blocking
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
self.ensure_manifest_exists()?;
self.ensure_contexts()?;
self.control_flag.set(DownloadThreadControlFlag::Go);
let mut db_lock = borrow_db_mut_checked();
db_lock.applications.transient_statuses.insert(
self.metadata(),
ApplicationTransientStatus::Downloading {
version_name: self.version.clone(),
},
);
push_game_update(
app_handle,
&self.metadata().id,
None,
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
);
Ok(())
}
// Blocking
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
self.setup_download(app_handle)?;
self.setup_download()?;
self.set_progress_object_params();
let timer = Instant::now();
info!("beginning download for {}...", self.metadata().id);
push_game_update(
app_handle,
&self.metadata().id,
None,
(
None,
Some(ApplicationTransientStatus::Downloading {
version_name: self.version.clone(),
}),
),
);
let res = self
.run()
.map_err(|()| ApplicationDownloadError::DownloadError);
.map_err(|_| ApplicationDownloadError::DownloadError);
debug!(
"{} took {}ms to download",
@ -144,7 +135,7 @@ impl GameDownloadAgent {
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
let header = generate_authorization_header();
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
&["/api/v1/client/game/manifest"],
@ -174,8 +165,12 @@ impl GameDownloadAgent {
Err(ApplicationDownloadError::Lock)
}
// Sets it up for both download and validate
fn setup_progress(&self) {
fn set_progress_object_params(&self) {
// Avoid re-setting it
if self.progress.get_max() != 0 {
return;
}
let contexts = self.contexts.lock().unwrap();
let length = contexts.len();
@ -184,7 +179,7 @@ impl GameDownloadAgent {
self.progress.set_max(chunk_count);
self.progress.set_size(length);
self.progress.reset();
self.progress.set_time_now();
}
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
@ -192,7 +187,10 @@ impl GameDownloadAgent {
self.generate_contexts()?;
}
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
self.context_map
.lock()
.unwrap()
.extend(self.stored_manifest.get_contexts());
Ok(())
}
@ -202,7 +200,7 @@ impl GameDownloadAgent {
let game_id = self.id.clone();
let mut contexts = Vec::new();
let base_path = Path::new(&self.dropdata.base_path);
let base_path = Path::new(&self.stored_manifest.base_path);
create_dir_all(base_path).unwrap();
for (raw_path, chunk) in manifest {
@ -211,12 +209,11 @@ impl GameDownloadAgent {
let container = path.parent().unwrap();
create_dir_all(container).unwrap();
let already_exists = path.exists();
let file = OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.create(true)
.truncate(false)
.open(path.clone())
.unwrap();
let mut running_offset = 0;
@ -237,12 +234,12 @@ impl GameDownloadAgent {
}
#[cfg(target_os = "linux")]
if running_offset > 0 && !already_exists {
if running_offset > 0 {
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
}
}
let existing_contexts = self.dropdata.get_completed_contexts();
self.dropdata.set_contexts(
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)))
@ -254,8 +251,8 @@ impl GameDownloadAgent {
Ok(())
}
fn run(&self) -> Result<bool, ()> {
self.setup_progress();
// TODO: Change return value on Err
pub fn run(&self) -> Result<bool, ()> {
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
@ -271,8 +268,9 @@ impl GameDownloadAgent {
let completed_indexes_loop_arc = completed_contexts.clone();
let contexts = self.contexts.lock().unwrap();
debug!("{contexts:#?}");
pool.scope(|scope| {
let client = &DROP_CLIENT_SYNC.clone();
let client = &reqwest::blocking::Client::new();
let context_map = self.context_map.lock().unwrap();
for (index, context) in contexts.iter().enumerate() {
let client = client.clone();
@ -282,10 +280,7 @@ impl GameDownloadAgent {
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it
// Note to future DecDuck, DropData gets loaded into context_map
if let Some(v) = context_map.get(&context.checksum)
&& *v
{
if Some(&true) == context_map.get(&context.checksum) {
progress_handle.skip(context.length);
continue;
}
@ -330,7 +325,7 @@ impl GameDownloadAgent {
}
});
let newly_completed = completed_contexts.clone();
let newly_completed = completed_contexts.to_owned();
let completed_lock_len = {
let mut context_map_lock = self.context_map.lock().unwrap();
@ -346,14 +341,14 @@ impl GameDownloadAgent {
.map(|x| {
(
x.checksum.clone(),
context_map_lock.get(&x.checksum).copied().unwrap_or(false),
context_map_lock.get(&x.checksum).cloned().unwrap_or(false),
)
})
.collect::<Vec<(String, bool)>>();
drop(context_map_lock);
self.dropdata.set_contexts(&contexts);
self.dropdata.write();
self.stored_manifest.set_contexts(&contexts);
self.stored_manifest.write();
// If there are any contexts left which are false
if !contexts.iter().all(|x| x.1) {
@ -368,93 +363,6 @@ impl GameDownloadAgent {
Ok(true)
}
fn setup_validate(&self, app_handle: &AppHandle) {
self.setup_progress();
self.control_flag.set(DownloadThreadControlFlag::Go);
let mut db_lock = borrow_db_mut_checked();
db_lock.applications.transient_statuses.insert(
self.metadata(),
ApplicationTransientStatus::Validating {
version_name: self.version.clone(),
},
);
push_game_update(
app_handle,
&self.metadata().id,
None,
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
);
}
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
self.setup_validate(app_handle);
let contexts = self.contexts.lock().unwrap();
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"validating game: {} with {} threads",
self.dropdata.game_id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
.unwrap();
let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| {
for (index, context) in contexts.iter().enumerate() {
let current_progress = self.progress.get(index);
let progress_handle = ProgressHandle::new(current_progress, self.progress.clone());
let invalid_chunks_scoped = invalid_chunks.clone();
let sender = self.sender.clone();
scope.spawn(move |_| {
match validate_game_chunk(context, &self.control_flag, progress_handle) {
Ok(true) => {}
Ok(false) => {
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", self.id);
for context in invalid_chunks.iter() {
self.dropdata.set_context(context.1.clone(), false);
}
self.dropdata.write();
return Ok(false);
}
Ok(true)
}
pub fn cancel(&self, app_handle: &AppHandle) {
// See docs on usage
set_partially_installed(
&self.metadata(),
self.dropdata.base_path.to_str().unwrap().to_string(),
Some(app_handle),
);
self.dropdata.write();
}
}
impl Downloadable for GameDownloadAgent {
@ -463,11 +371,6 @@ impl Downloadable for GameDownloadAgent {
self.download(app_handle)
}
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating;
self.validate(app_handle)
}
fn progress(&self) -> Arc<ProgressObject> {
self.progress.clone()
}
@ -506,25 +409,36 @@ impl Downloadable for GameDownloadAgent {
fn on_complete(&self, app_handle: &tauri::AppHandle) {
on_game_complete(
&self.metadata(),
self.dropdata.base_path.to_string_lossy().to_string(),
self.stored_manifest.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
}
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
self.cancel(app_handle);
/*
on_game_incomplete(
&self.metadata(),
self.dropdata.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
*/
// 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) {
on_game_incomplete(
&self.metadata(),
self.stored_manifest.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
}
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone()
}
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Validating;
game_validate_logic(
&self.stored_manifest,
self.contexts.lock().unwrap().clone(),
self.progress.clone(),
self.sender.clone(),
&self.control_flag,
)
}
}

View File

@ -27,9 +27,8 @@ pub struct DropWriter<W: Write> {
}
impl DropWriter<File> {
fn new(path: PathBuf) -> Self {
let destination = OpenOptions::new().write(true).create(true).truncate(false).open(&path).unwrap();
Self {
destination,
destination: OpenOptions::new().write(true).open(path).unwrap(),
hasher: Context::new(),
}
}
@ -151,7 +150,7 @@ pub fn download_game_chunk(
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err),
));
}
};
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::UnparseableResponse(raw_res),
));
@ -187,7 +186,7 @@ pub fn download_game_chunk(
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
if !completed {
return Ok(false);
}
};
// If we complete the file, set the permissions (if on Linux)
#[cfg(unix)]

View File

@ -1,16 +1,18 @@
use std::{
collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
fs::File,
io::{Read, Write},
path::PathBuf,
};
use log::error;
use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
pub type DropData = v1::DropData;
pub static DROP_DATA_PATH: &str = ".dropdata";
static DROP_DATA_PATH: &str = ".dropdata";
pub mod v1 {
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
use std::{path::PathBuf, sync::Mutex};
use native_model::native_model;
use serde::{Deserialize, Serialize};
@ -20,7 +22,7 @@ pub mod v1 {
pub struct DropData {
pub game_id: String,
pub game_version: String,
pub contexts: Mutex<HashMap<String, bool>>,
pub contexts: Mutex<Vec<(String, bool)>>,
pub base_path: PathBuf,
}
@ -30,7 +32,7 @@ pub mod v1 {
base_path,
game_id,
game_version,
contexts: Mutex::new(HashMap::new()),
contexts: Mutex::new(Vec::new()),
}
}
}
@ -38,18 +40,30 @@ pub mod v1 {
impl DropData {
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
match DropData::read(&base_path) {
Ok(v) => v,
Err(_) => DropData::new(game_id, game_version, base_path),
}
}
pub fn read(base_path: &Path) -> Result<Self, io::Error> {
let mut file = File::open(base_path.join(DROP_DATA_PATH))?;
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();
file.read_to_end(&mut s)?;
match file.read_to_end(&mut s) {
Ok(_) => {}
Err(e) => {
error!("{e}");
return DropData::new(game_id, game_version, base_path);
}
};
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
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) {
@ -66,25 +80,26 @@ impl DropData {
};
match file.write_all(&manifest_raw) {
Ok(()) => {}
Ok(_) => {}
Err(e) => error!("{e}"),
}
};
}
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
*self.contexts.lock().unwrap() = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();
}
pub fn set_context(&self, context: String, state: bool) {
self.contexts.lock().unwrap().entry(context).insert_entry(state);
*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 })
.filter_map(|x| if x.1 { Some(x.0.clone()) } else { None })
.collect()
}
pub fn get_contexts(&self) -> HashMap<String, bool> {
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,6 +1,6 @@
pub mod commands;
pub mod download_agent;
mod download_logic;
pub mod drop_data;
mod drop_data;
mod manifest;
pub mod validate;

View File

@ -1,22 +1,91 @@
use std::{
fs::File,
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
sync::{mpsc::Sender, Arc},
};
use log::debug;
use log::{debug, error, info};
use md5::Context;
use rayon::ThreadPoolBuilder;
use crate::{
download_manager::
database::db::borrow_db_checked,
download_manager::{
download_manager_frontend::DownloadManagerSignal,
util::{
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
progress_object::ProgressHandle,
}
,
progress_object::{ProgressHandle, ProgressObject},
},
},
error::application_download_error::ApplicationDownloadError,
games::downloads::manifest::DropDownloadContext,
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
};
pub fn game_validate_logic(
dropdata: &DropData,
contexts: Vec<DropDownloadContext>,
progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
control_flag: &DownloadThreadControl,
) -> Result<bool, ApplicationDownloadError> {
progress.reset(contexts.len());
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"validating game: {} with {} threads",
dropdata.game_id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
.unwrap();
debug!("{contexts:#?}");
let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| {
for (index, context) in contexts.iter().enumerate() {
let current_progress = progress.get(index);
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
let invalid_chunks_scoped = invalid_chunks.clone();
let sender = sender.clone();
scope.spawn(move |_| {
match validate_game_chunk(context, control_flag, progress_handle) {
Ok(true) => {
debug!(
"Finished context #{} with checksum {}",
index, context.checksum
);
}
Ok(false) => {
debug!(
"Didn't finish context #{} with checksum {}",
index, &context.checksum
);
invalid_chunks_scoped.push(context.checksum.clone());
}
Err(e) => {
error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
});
}
});
// If there are any contexts left which are false
if !invalid_chunks.is_empty() {
info!(
"validation of game id {} failed for chunks {:?}",
dropdata.game_id.clone(),
invalid_chunks
);
return Ok(false);
}
Ok(true)
}
pub fn validate_game_chunk(
ctx: &DropDownloadContext,
control_flag: &DownloadThreadControl,
@ -32,9 +101,7 @@ pub fn validate_game_chunk(
return Ok(false);
}
let Ok(mut source) = File::open(&ctx.path) else {
return Ok(false);
};
let mut source = File::open(&ctx.path).unwrap();
if ctx.offset != 0 {
source
@ -48,10 +115,14 @@ pub fn validate_game_chunk(
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);
}

View File

@ -7,9 +7,7 @@ use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use tauri::Emitter;
use crate::AppState;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::Database;
use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
};
@ -18,11 +16,9 @@ use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::cache_object_db;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::make_request;
use crate::remote::utils::DROP_CLIENT_SYNC;
use bitcode::{Decode, Encode};
use crate::AppState;
#[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct {
@ -31,7 +27,7 @@ pub struct FetchGameStruct {
version: Option<GameVersion>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct Game {
id: String,
@ -81,7 +77,7 @@ pub fn fetch_library_logic(
) -> Result<Vec<Game>, RemoteAccessError> {
let header = generate_authorization_header();
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
f.header("Authorization", header)
})?
@ -99,7 +95,7 @@ pub fn fetch_library_logic(
let mut db_handle = borrow_db_mut_checked();
for game in &games {
for game in games.iter() {
handle.games.insert(game.id.clone(), game.clone());
if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle
@ -116,7 +112,7 @@ pub fn fetch_library_logic(
}
// We should always have a cache of the object
// Pass db_handle because otherwise we get a gridlock
let game = get_cached_object_db::<Game>(&meta.id.clone(), &db_handle)?;
let game = get_cached_object_db::<String, Game>(meta.id.clone(), &db_handle)?;
games.push(game);
}
@ -148,22 +144,27 @@ pub fn fetch_game_logic(
) -> Result<FetchGameStruct, RemoteAccessError> {
let mut state_handle = state.lock().unwrap();
let db_lock = borrow_db_checked();
let handle = borrow_db_checked();
let metadata_option = db_lock.applications.installed_game_version.get(&id);
let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => db_lock
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.cloned(),
Some(metadata) => Some(
handle
.applications
.game_versions
.get(&metadata.id)
.unwrap()
.get(metadata.version.as_ref().unwrap())
.unwrap()
.clone(),
),
};
drop(handle);
let game = state_handle.games.get(&id);
if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id, &db_lock);
let status = GameStatusManager::fetch_state(&id);
let data = FetchGameStruct {
game: game.clone(),
@ -171,13 +172,11 @@ pub fn fetch_game_logic(
version,
};
cache_object_db(&id, game, &db_lock)?;
cache_object(id, game)?;
return Ok(data);
}
drop(db_lock);
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
@ -202,18 +201,17 @@ pub fn fetch_game_logic(
.game_statuses
.entry(id.clone())
.or_insert(GameDownloadStatus::Remote {});
let status = GameStatusManager::fetch_state(&id, &db_handle);
drop(db_handle);
let status = GameStatusManager::fetch_state(&id);
let data = FetchGameStruct {
game: game.clone(),
status,
version,
};
cache_object(&id, &game)?;
cache_object(id, &game)?;
Ok(data)
}
@ -222,12 +220,12 @@ pub fn fetch_game_logic_offline(
id: String,
_state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let db_handle = borrow_db_checked();
let metadata_option = db_handle.applications.installed_game_version.get(&id);
let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => Some(
db_handle
handle
.applications
.game_versions
.get(&metadata.id)
@ -237,11 +235,10 @@ pub fn fetch_game_logic_offline(
.clone(),
),
};
drop(handle);
let status = GameStatusManager::fetch_state(&id, &db_handle);
let game = get_cached_object::<Game>(&id)?;
drop(db_handle);
let status = GameStatusManager::fetch_state(&id);
let game = get_cached_object::<String, Game>(id)?;
Ok(FetchGameStruct {
game,
@ -254,7 +251,7 @@ pub fn fetch_game_verion_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
@ -276,11 +273,7 @@ pub fn fetch_game_verion_options_logic(
let process_manager_lock = state_lock.process_manager.lock().unwrap();
let data: Vec<GameVersion> = data
.into_iter()
.filter(|v| {
process_manager_lock
.valid_platform(&v.platform, &state_lock)
.unwrap()
})
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
.collect();
drop(process_manager_lock);
drop(state_lock);
@ -288,49 +281,6 @@ pub fn fetch_game_verion_options_logic(
Ok(data)
}
/**
* Called by:
* - on_cancel, when cancelled, for obvious reasons
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
* - when scanning, to import the game
*/
pub fn set_partially_installed(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
}
pub fn set_partially_installed_db(
db_lock: &mut Database,
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
db_lock.applications.transient_statuses.remove(meta);
db_lock.applications.game_statuses.insert(
meta.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.as_ref().unwrap().clone(),
install_dir,
},
);
db_lock
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
if let Some(app_handle) = app_handle {
push_game_update(
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, db_lock),
);
}
}
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
debug!("triggered uninstall for agent");
let mut db_handle = borrow_db_mut_checked();
@ -344,7 +294,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
(None, Some(ApplicationTransientStatus::Uninstalling {})),
);
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
@ -378,10 +328,11 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
drop(db_handle);
let app_handle = app_handle.clone();
spawn(move || {
if let Err(e) = remove_dir_all(install_dir) {
spawn(move || match remove_dir_all(install_dir) {
Err(e) => {
error!("{e}");
} else {
}
Ok(_) => {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
@ -393,23 +344,21 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
.game_statuses
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
let _ = db_handle.applications.transient_statuses.remove(&meta);
drop(db_handle);
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
push_game_update(
&app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
(Some(GameDownloadStatus::Remote {}), None),
);
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
drop(db_handle);
}
});
} else {
warn!("invalid previous state for uninstall, failing silently.");
warn!("invalid previous state for uninstall, failing silently.")
}
}
@ -421,6 +370,66 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
.cloned()
}
pub fn on_game_incomplete(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", generate_authorization_header()),
)?
.send()?;
let game_version: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
.entry(meta.id.clone())
.or_default()
.insert(meta.version.clone().unwrap(), game_version.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
let status = GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.clone().unwrap(),
install_dir,
};
handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(handle);
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
},
)
.unwrap();
Ok(())
}
pub fn on_game_complete(
meta: &DownloadableMetadata,
install_dir: String,
@ -433,7 +442,7 @@ pub fn on_game_complete(
let header = generate_authorization_header();
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
&["/api/v1/client/game/version"],

View File

@ -1,4 +1,7 @@
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus};
use crate::database::{
db::borrow_db_checked,
models::data::{ApplicationTransientStatus, GameDownloadStatus},
};
pub type GameStatusWithTransient = (
Option<GameDownloadStatus>,
@ -7,12 +10,14 @@ pub type GameStatusWithTransient = (
pub struct GameStatusManager {}
impl GameStatusManager {
pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient {
let online_state = match database.applications.installed_game_version.get(game_id) {
Some(meta) => database.applications.transient_statuses.get(meta).cloned(),
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
let db_lock = borrow_db_checked();
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
None => None,
};
let offline_state = database.applications.game_statuses.get(game_id).cloned();
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
drop(db_lock);
if online_state.is_some() {
return (None, online_state);

View File

@ -1,5 +1,4 @@
#![feature(fn_traits)]
#![feature(duration_constructors)]
#![deny(clippy::all)]
mod database;
@ -11,12 +10,7 @@ mod error;
mod process;
mod remote;
use crate::database::scan::scan_install_dirs;
use crate::process::commands::open_process_logs;
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
use crate::remote::commands::auth_initiate_code;
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use bitcode::{Decode, Encode};
use client::commands::fetch_state;
use client::{
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
@ -26,13 +20,13 @@ use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
fetch_system_data, update_settings,
};
use database::db::{DATA_ROOT_DIR, DatabaseInterface, borrow_db_checked, borrow_db_mut_checked};
use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, DATA_ROOT_DIR};
use database::models::data::GameDownloadStatus;
use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
};
use download_manager::download_manager_builder::DownloadManagerBuilder;
use download_manager::download_manager_frontend::DownloadManager;
use download_manager::download_manager_builder::DownloadManagerBuilder;
use games::collections::commands::{
add_game_to_collection, create_collection, delete_collection, delete_game_in_collection,
fetch_collection, fetch_collections,
@ -41,13 +35,13 @@ use games::commands::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
};
use games::downloads::commands::download_game;
use games::library::{Game, update_game_configuration};
use log::{LevelFilter, debug, info, warn};
use log4rs::Config;
use games::library::{update_game_configuration, Game};
use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
use process::commands::{kill_game, launch_game};
use process::process_manager::ProcessManager;
use remote::auth::{self, recieve_handshake};
@ -55,14 +49,13 @@ use remote::commands::{
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect,
sign_out, use_remote,
};
use remote::fetch_object::fetch_object;
use remote::fetch_object::{fetch_object, fetch_object_offline};
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
use std::panic::PanicHookInfo;
use std::path::Path;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
@ -88,7 +81,7 @@ pub enum AppStatus {
ServerUnavailable,
}
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
@ -98,27 +91,6 @@ pub struct User {
profile_picture_object_id: String,
}
#[derive(Clone)]
pub struct CompatInfo {
umu_installed: bool,
}
fn create_new_compat_info() -> Option<CompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE)
.stdout(Stdio::null())
.spawn();
if let Err(umu_error) = &has_umu_installed {
warn!("disabling windows support with error: {umu_error}");
}
let has_umu_installed = has_umu_installed.is_ok();
Some(CompatInfo {
umu_installed: has_umu_installed,
})
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppState<'a> {
@ -130,8 +102,6 @@ pub struct AppState<'a> {
download_manager: Arc<DownloadManager>,
#[serde(skip_serializing)]
process_manager: Arc<Mutex<ProcessManager<'a>>>,
#[serde(skip_serializing)]
compat_info: Option<CompatInfo>,
}
fn setup(handle: AppHandle) -> AppState<'static> {
@ -168,13 +138,9 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let games = HashMap::new();
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
let compat_info = create_new_compat_info();
debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
scan_install_dirs();
if !is_set_up {
return AppState {
status: AppStatus::NotConfigured,
@ -182,7 +148,6 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
compat_info,
};
}
@ -195,8 +160,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let mut missing_games = Vec::new();
let statuses = db_handle.applications.game_statuses.clone();
drop(db_handle);
for (game_id, status) in statuses {
for (game_id, status) in statuses.into_iter() {
match status {
GameDownloadStatus::Remote {} => {}
GameDownloadStatus::PartiallyInstalled { .. } => {}
@ -247,7 +211,6 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
compat_info,
}
}
@ -262,8 +225,7 @@ pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
.as_secs()
));
let mut file = File::create_new(crash_file).ok()?;
file.write_all(format!("Drop crashed with the following panic:\n{e}").as_bytes())
.ok()?;
file.write_all(format!("Drop crashed with the following panic:\n{e}").as_bytes()).ok()?;
drop(file);
Some(())
@ -273,11 +235,11 @@ pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
pub fn run() {
panic::set_hook(Box::new(|e| {
let _ = custom_panic_handler(e);
println!("{e}");
let dft = panic::take_hook();
dft.call((e,));
}));
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init());
@ -301,7 +263,6 @@ pub fn run() {
fetch_settings,
// Auth
auth_initiate,
auth_initiate_code,
retry_connect,
manual_recieve_handshake,
sign_out,
@ -338,7 +299,6 @@ pub fn run() {
kill_game,
toggle_autostart,
get_autostart_enabled,
open_process_logs
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
@ -379,7 +339,7 @@ pub fn run() {
let binding = event.urls();
let url = binding.first().unwrap();
if url.host_str().unwrap() == "handshake" {
recieve_handshake(handle.clone(), url.path().to_string());
recieve_handshake(handle.clone(), url.path().to_string())
}
});
@ -436,10 +396,15 @@ pub fn run() {
Ok(())
})
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
tauri::async_runtime::spawn(async move {
fetch_object(request, responder).await;
});
.register_asynchronous_uri_scheme_protocol("object", move |ctx, request, responder| {
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
offline!(
state,
fetch_object,
fetch_object_offline,
request,
responder
);
})
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| {
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();

View File

@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
drop_app_lib::run();
drop_app_lib::run()
}

View File

@ -16,10 +16,10 @@ pub fn launch_game(
// download_type: DownloadType::Game,
//};
match process_manager_lock.launch_process(id, &state_lock) {
Ok(()) => {}
match process_manager_lock.launch_process(id) {
Ok(_) => {}
Err(e) => return Err(e),
}
};
drop(process_manager_lock);
drop(state_lock);
@ -38,13 +38,3 @@ pub fn kill_game(
.kill_game(game_id)
.map_err(ProcessError::IOError)
}
#[tauri::command]
pub fn open_process_logs(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock.open_process_logs(game_id)
}

View File

@ -1,33 +0,0 @@
use std::collections::HashMap;
use dynfmt::{Argument, FormatArgs};
pub struct DropFormatArgs {
positional: Vec<String>,
map: HashMap<&'static str, String>,
}
impl DropFormatArgs {
pub fn new(launch_string: String, working_dir: &String, executable_name: &String, absolute_executable_name: String) -> Self {
let mut positional = Vec::new();
let mut map: HashMap<&'static str, String> = HashMap::new();
positional.push(launch_string);
map.insert("dir", working_dir.to_string());
map.insert("exe", executable_name.to_string());
map.insert("abs_exe", absolute_executable_name);
Self { positional, map }
}
}
impl FormatArgs for DropFormatArgs {
fn get_index(&self, index: usize) -> Result<Option<dynfmt::Argument<'_>>, ()> {
Ok(self.positional.get(index).map(|arg| arg as Argument<'_>))
}
fn get_key(&self, key: &str) -> Result<Option<dynfmt::Argument<'_>>, ()> {
Ok(self.map.get(key).map(|arg| arg as Argument<'_>))
}
}

View File

@ -2,5 +2,3 @@ pub mod commands;
#[cfg(target_os = "linux")]
pub mod compat;
pub mod process_manager;
pub mod process_handlers;
pub mod format;

View File

@ -1,109 +0,0 @@
use log::debug;
use crate::{
AppState,
database::models::data::{Database, DownloadableMetadata, GameVersion},
process::process_manager::{Platform, ProcessHandler},
};
pub struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
_game_version: &GameVersion,
_current_dir: &str,
) -> String {
format!("\"{}\" {}", launch_command, args.join(" "))
}
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
true
}
}
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
pub struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
_current_dir: &str,
) -> String {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => {
if game_override.is_empty() {
game_version.game_id.clone()
} else {
game_override.clone()
}
}
None => game_version.game_id.clone(),
};
format!(
"GAMEID={game_id} {umu} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE,
launch = launch_command,
args = args.join(" ")
)
}
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
let Some(ref compat_info) = state.compat_info else {
return false;
};
compat_info.umu_installed
}
}
pub struct AsahiMuvmLauncher;
impl ProcessHandler for AsahiMuvmLauncher {
fn create_launch_process(
&self,
meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
current_dir: &str,
) -> String {
let umu_launcher = UMULauncher {};
let umu_string = umu_launcher.create_launch_process(
meta,
launch_command,
args,
game_version,
current_dir,
);
let mut args_cmd = umu_string.split("umu-run").collect::<Vec<&str>>().into_iter();
let args = args_cmd.next().unwrap().trim();
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
format!("{args} muvm -- {cmd}")
}
#[allow(unreachable_code)]
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
#[cfg(not(target_os = "linux"))]
return false;
#[cfg(not(target_arch = "aarch64"))]
return false;
let page_size = page_size::get();
if page_size != 16384 {
return false;
}
let Some(ref compat_info) = state.compat_info else {
return false;
};
compat_info.umu_installed
}
}

View File

@ -1,13 +1,12 @@
use std::{
collections::HashMap,
fs::{OpenOptions, create_dir_all},
fs::OpenOptions,
io::{self},
path::PathBuf,
process::{Command, ExitStatus},
str::FromStr,
sync::{Arc, Mutex},
thread::spawn,
time::{Duration, SystemTime},
};
use dynfmt::Format;
@ -15,41 +14,27 @@ use dynfmt::SimpleCurlyFormat;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use shared_child::SharedChild;
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_opener::OpenerExt;
use tauri::{AppHandle, Manager};
use crate::{
AppState, DB,
database::{
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
db::{borrow_db_mut_checked, DATA_ROOT_DIR},
models::data::{
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
GameDownloadStatus, GameVersion,
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
GameVersion,
},
},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
process::{
format::DropFormatArgs,
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
},
AppState, DB,
};
pub struct RunningProcess {
handle: Arc<SharedChild>,
start: SystemTime,
manually_killed: bool,
}
pub struct ProcessManager<'a> {
current_platform: Platform,
log_output_dir: PathBuf,
processes: HashMap<String, RunningProcess>,
processes: HashMap<String, Arc<SharedChild>>,
app_handle: AppHandle,
game_launchers: Vec<(
(Platform, Platform),
&'a (dyn ProcessHandler + Sync + Send + 'static),
)>,
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
}
impl ProcessManager<'_> {
@ -69,7 +54,7 @@ impl ProcessManager<'_> {
app_handle,
processes: HashMap::new(),
log_output_dir,
game_launchers: vec![
game_launchers: HashMap::from([
// Current platform to target platform
(
(Platform::Windows, Platform::Windows),
@ -83,24 +68,19 @@ impl ProcessManager<'_> {
(Platform::MacOs, Platform::MacOs),
&NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
(
(Platform::Linux, Platform::Windows),
&AsahiMuvmLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
(
(Platform::Linux, Platform::Windows),
&UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
),
],
]),
}
}
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
match self.processes.get_mut(&game_id) {
Some(process) => {
process.manually_killed = true;
process.handle.kill()?;
process.handle.wait()?;
match self.processes.get(&game_id) {
Some(child) => {
child.kill()?;
child.wait()?;
Ok(())
}
None => Err(io::Error::new(
@ -110,30 +90,15 @@ impl ProcessManager<'_> {
}
}
fn get_log_dir(&self, game_id: String) -> PathBuf {
self.log_output_dir.join(game_id)
}
pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> {
let dir = self.get_log_dir(game_id);
self.app_handle
.opener()
.open_path(dir.to_str().unwrap(), None::<&str>)
.map_err(ProcessError::OpenerError)?;
Ok(())
}
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
if !self.processes.contains_key(&game_id) {
warn!(
"process on_finish was called, but game_id is no longer valid. finished with result: {result:?}"
);
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {result:?}");
return;
}
debug!("process for {:?} exited with {:?}", &game_id, result);
let process = self.processes.remove(&game_id).unwrap();
self.processes.remove(&game_id);
let mut db_handle = borrow_db_mut_checked();
let meta = db_handle
@ -149,65 +114,34 @@ impl ProcessManager<'_> {
version_name,
install_dir,
}) = current_state
&& let Ok(exit_code) = result
&& exit_code.success()
{
db_handle.applications.game_statuses.insert(
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
},
);
if let Ok(exit_code) = result {
if exit_code.success() {
db_handle.applications.game_statuses.insert(
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
},
);
}
}
}
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
// If we started and ended really quickly, something might've gone wrong
// Or if the status isn't 0
// Or if it's an error
if !process.manually_killed
&& (elapsed.as_secs() <= 2 || result.is_err() || !result.unwrap().success())
{
warn!("drop detected that the game {game_id} may have failed to launch properly");
let _ = self.app_handle.emit("launch_external_error", &game_id);
}
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
drop(db_handle);
let status = GameStatusManager::fetch_state(&game_id);
push_game_update(&self.app_handle, &game_id, None, status);
// TODO better management
}
fn fetch_process_handler(
&self,
db_lock: &Database,
state: &AppState,
target_platform: &Platform,
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
Ok(self
.game_launchers
.iter()
.find(|e| {
let (e_current, e_target) = e.0;
e_current == self.current_platform
&& e_target == *target_platform
&& e.1.valid_for_platform(db_lock, state, target_platform)
})
.ok_or(ProcessError::InvalidPlatform)?
.1)
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
let current = &self.current_platform;
Ok(self.game_launchers.contains_key(&(*current, *platform)))
}
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
let db_lock = borrow_db_checked();
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
Ok(process_handler.is_ok())
}
pub fn launch_process(
&mut self,
game_id: String,
state: &AppState,
) -> Result<(), ProcessError> {
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
if self.processes.contains_key(&game_id) {
return Err(ProcessError::AlreadyRunning);
}
@ -221,7 +155,9 @@ impl ProcessManager<'_> {
.cloned()
{
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
Some(GameDownloadStatus::SetupRequired { version_name, .. }) => version_name,
Some(GameDownloadStatus::SetupRequired { .. }) => {
return Err(ProcessError::SetupRequired)
}
_ => return Err(ProcessError::NotInstalled),
};
let meta = DownloadableMetadata {
@ -231,6 +167,10 @@ impl ProcessManager<'_> {
};
let mut db_lock = borrow_db_mut_checked();
debug!(
"Launching process {:?} with games {:?}",
&game_id, db_lock.applications.game_versions
);
let game_status = db_lock
.applications
@ -247,15 +187,13 @@ impl ProcessManager<'_> {
version_name,
install_dir,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotInstalled),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded),
};
debug!(
"Launching process {:?} with version {:?}",
&game_id,
db_lock.applications.game_versions.get(&game_id).unwrap()
);
let game_version = db_lock
.applications
.game_versions
@ -264,17 +202,18 @@ impl ProcessManager<'_> {
.get(version_name)
.ok_or(ProcessError::InvalidVersion)?;
// TODO: refactor this path with open_process_logs
let game_log_folder = &self.get_log_dir(game_id);
create_dir_all(game_log_folder).map_err(ProcessError::IOError)?;
let current_time = chrono::offset::Local::now();
let log_file = OpenOptions::new()
.write(true)
.truncate(true)
.read(true)
.create(true)
.open(game_log_folder.join(format!("{}-{}.log", &version, current_time.timestamp())))
.open(self.log_output_dir.join(format!(
"{}-{}-{}.log",
&game_id,
&version,
current_time.timestamp()
)))
.map_err(ProcessError::IOError)?;
let error_file = OpenOptions::new()
@ -282,16 +221,21 @@ impl ProcessManager<'_> {
.truncate(true)
.read(true)
.create(true)
.open(game_log_folder.join(format!(
"{}-{}-error.log",
.open(self.log_output_dir.join(format!(
"{}-{}-{}-error.log",
&game_id,
&version,
current_time.timestamp()
)))
.map_err(ProcessError::IOError)?;
let current_platform = self.current_platform;
let target_platform = game_version.platform;
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
let game_launcher = self
.game_launchers
.get(&(current_platform, target_platform))
.ok_or(ProcessError::InvalidPlatform)?;
let (launch, args) = match game_status {
GameDownloadStatus::Installed {
@ -312,23 +256,16 @@ impl ProcessManager<'_> {
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
let launch = launch.to_str().unwrap();
let launch_string = process_handler.create_launch_process(
let launch_string = game_launcher.create_launch_process(
&meta,
launch.to_string(),
args.clone(),
args.to_vec(),
game_version,
install_dir,
);
let format_args = DropFormatArgs::new(
launch_string,
install_dir,
&game_version.launch_command,
launch.to_string(),
);
let launch_string = SimpleCurlyFormat
.format(&game_version.launch_command_template, format_args)
.format(&game_version.launch_command_template, &[launch_string])
.map_err(|e| ProcessError::FormatError(e.to_string()))?
.to_string();
@ -345,14 +282,11 @@ impl ProcessManager<'_> {
#[cfg(unix)]
let mut command: Command = Command::new("sh");
#[cfg(unix)]
command.args(vec!["-c", &launch_string]);
debug!("final launch string:\n\n{launch_string}\n");
command.arg("-c").arg(launch_string);
command
.stderr(error_file)
.stdout(log_file)
.env_remove("RUST_LOG")
.current_dir(install_dir);
let child = command.spawn().map_err(ProcessError::IOError)?;
@ -391,14 +325,7 @@ impl ProcessManager<'_> {
drop(app_state_handle);
});
self.processes.insert(
meta.id,
RunningProcess {
handle: wait_thread_handle,
start: SystemTime::now(),
manually_killed: false,
},
);
self.processes.insert(meta.id, wait_thread_handle);
Ok(())
}
}
@ -457,6 +384,49 @@ pub trait ProcessHandler: Send + 'static {
game_version: &GameVersion,
current_dir: &str,
) -> String;
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
}
struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
_game_version: &GameVersion,
_current_dir: &str,
) -> String {
format!("\"{}\" {}", launch_command, args.join(" "))
}
}
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn create_launch_process(
&self,
_meta: &DownloadableMetadata,
launch_command: String,
args: Vec<String>,
game_version: &GameVersion,
_current_dir: &str,
) -> String {
debug!("Game override: \"{:?}\"", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => {
if game_override.is_empty() {
game_version.game_id.clone()
} else {
game_override.clone()
}
}
None => game_version.game_id.clone(),
};
format!(
"GAMEID={game_id} {umu} \"{launch}\" {args}",
umu = UMU_LAUNCHER_EXECUTABLE,
launch = launch_command,
args = args.join(" ")
)
}
}

View File

@ -12,7 +12,9 @@ use crate::{
database::{
db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth,
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::utils::DROP_CLIENT_SYNC, AppState, AppStatus, User
},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User,
};
use super::{
@ -30,7 +32,6 @@ struct InitiateRequestBody {
name: String,
platform: String,
capabilities: HashMap<String, CapabilityConfiguration>,
mode: String,
}
#[derive(Serialize)]
@ -64,7 +65,7 @@ pub fn generate_authorization_header() -> String {
pub fn fetch_user() -> Result<User, RemoteAccessError> {
let header = generate_authorization_header();
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
f.header("Authorization", header)
})?
@ -80,11 +81,11 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
return Err(RemoteAccessError::InvalidResponse(err));
}
response.json::<User>().map_err(std::convert::Into::into)
response.json::<User>().map_err(|e| e.into())
}
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
let path_chunks: Vec<&str> = path.split('/').collect();
let path_chunks: Vec<&str> = path.split("/").collect();
if path_chunks.len() != 3 {
app.emit("auth/failed", ()).unwrap();
return Err(RemoteAccessError::HandshakeFailed(
@ -100,12 +101,12 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let client_id = path_chunks.get(1).unwrap();
let token = path_chunks.get(2).unwrap();
let body = HandshakeRequestBody {
client_id: (*client_id).to_string(),
token: (*token).to_string(),
client_id: client_id.to_string(),
token: token.to_string(),
};
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?;
debug!("handshake responsded with {}", response.status().as_u16());
if !response.status().is_success() {
@ -165,7 +166,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
app.emit("auth/finished", ()).unwrap();
}
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
@ -181,10 +182,9 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
]),
mode,
};
let client = DROP_CLIENT_SYNC.clone();
let client = reqwest::blocking::Client::new();
let response = client.post(endpoint.to_string()).json(&body).send()?;
if response.status() != 200 {
@ -194,9 +194,13 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
let response = response.text()?;
let redir_url = response.text()?;
let complete_redir_url = base_url.join(&redir_url)?;
Ok(response)
debug!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap();
Ok(())
}
pub fn setup() -> (AppStatus, Option<User>) {
@ -208,7 +212,7 @@ pub fn setup() -> (AppStatus, Option<User>) {
let user_result = match fetch_user() {
Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => {
let user = get_cached_object::<User>("user").unwrap();
let user = get_cached_object::<_, User>("user").unwrap();
return (AppStatus::Offline, Some(user));
}
Err(_) => return (AppStatus::SignedInNeedsReauth, None),

View File

@ -1,17 +1,11 @@
use std::{
fs::File,
io::{self, Write},
path::{Path, PathBuf},
time::SystemTime,
};
use crate::{
database::{db::borrow_db_checked, models::data::Database},
error::remote_access_error::RemoteAccessError,
};
use bitcode::{Decode, DecodeOwned, Encode};
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::debug;
use cacache::Integrity;
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
#[macro_export]
macro_rules! offline {
@ -25,75 +19,31 @@ macro_rules! offline {
}
}
fn get_sys_time_in_secs() -> u64 {
match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
}
}
fn get_cache_path(base: &Path, key: &str) -> PathBuf {
let key_hash = hex::encode(md5::compute(key.as_bytes()).0);
base.join(key_hash)
}
fn write_sync(base: &Path, key: &str, data: Vec<u8>) -> io::Result<()> {
let cache_path = get_cache_path(base, key);
let mut file = File::create(cache_path)?;
file.write_all(&data)?;
Ok(())
}
fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
let cache_path = get_cache_path(base, key);
let file = std::fs::read(cache_path)?;
Ok(file)
}
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
cache_object_db(key, data, &borrow_db_checked())
}
pub fn cache_object_db<D: Encode>(
key: &str,
pub fn cache_object<K: AsRef<str>, D: Serialize + DeserializeOwned>(
key: K,
data: &D,
database: &Database,
) -> Result<(), RemoteAccessError> {
let bytes = bitcode::encode(data);
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
) -> Result<Integrity, RemoteAccessError> {
let bytes = serde_binary::to_vec(data, Endian::Little).unwrap();
cacache::write_sync(&borrow_db_checked().cache_dir, key, bytes)
.map_err(RemoteAccessError::Cache)
}
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
get_cached_object_db::<D>(key, &borrow_db_checked())
pub fn get_cached_object<K: AsRef<str>, D: Serialize + DeserializeOwned>(
key: K,
) -> Result<D, RemoteAccessError> {
get_cached_object_db::<K, D>(key, &borrow_db_checked())
}
pub fn get_cached_object_db<D: DecodeOwned>(
key: &str,
pub fn get_cached_object_db<K: AsRef<str>, D: Serialize + DeserializeOwned>(
key: K,
db: &Database,
) -> Result<D, RemoteAccessError> {
let start = SystemTime::now();
let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
let read = start.elapsed().unwrap();
let data =
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
let decode = start.elapsed().unwrap();
debug!(
"cache object took: r:{}, d:{}, b:{}",
read.as_millis(),
read.abs_diff(decode).as_millis(),
bytes.len()
);
let bytes = cacache::read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
let data = serde_binary::from_slice::<D>(&bytes, Endian::Little).unwrap();
Ok(data)
}
#[derive(Encode, Decode)]
#[derive(Serialize, Deserialize)]
pub struct ObjectCache {
content_type: String,
body: Vec<u8>,
expiry: u64,
}
impl ObjectCache {
pub fn has_expired(&self) -> bool {
let current = get_sys_time_in_secs();
self.expiry < current
}
}
impl From<Response<Vec<u8>>> for ObjectCache {
@ -107,7 +57,6 @@ impl From<Response<Vec<u8>>> for ObjectCache {
.unwrap()
.to_owned(),
body: value.body().clone(),
expiry: get_sys_time_in_secs() + 60 * 60 * 24,
}
}
}
@ -117,9 +66,3 @@ impl From<ObjectCache> for Response<Vec<u8>> {
resp_builder.body(value.body).unwrap()
}
}
impl From<&ObjectCache> for Response<Vec<u8>> {
fn from(value: &ObjectCache) -> Self {
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
resp_builder.body(value.body.clone()).unwrap()
}
}

View File

@ -1,14 +1,15 @@
use std::sync::Mutex;
use futures_lite::StreamExt;
use log::{debug, warn};
use reqwest_websocket::{Message, RequestBuilderExt};
use serde::Deserialize;
use log::debug;
use reqwest::blocking::Client;
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC}, 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::{
@ -41,7 +42,7 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
#[tauri::command]
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
let _drop_url = gen_drop_url(path.clone())?;
let req = make_request(&DROP_CLIENT_SYNC, &[&path], &[], |r| {
let req = make_request(&Client::new(), &[&path], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
.send();
@ -54,7 +55,7 @@ pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
}
Err(e) => {
debug!("{e}");
get_cached_object::<Vec<u8>>(&path)
get_cached_object::<&str, Vec<u8>>(&path)
}
}
}
@ -90,78 +91,7 @@ pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
#[tauri::command]
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
let redir_url = auth_initiate_logic("callback".to_string())?;
let complete_redir_url = base_url.join(&redir_url)?;
debug!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap();
Ok(())
}
#[derive(Deserialize)]
struct CodeWebsocketResponse {
#[serde(rename = "type")]
response_type: String,
value: String,
}
#[tauri::command]
pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
let code = auth_initiate_logic("code".to_string())?;
let header_code = code.clone();
tauri::async_runtime::spawn(async move {
let load = async || -> Result<(), RemoteAccessError> {
let ws_url = base_url.join("/api/v1/client/auth/code/ws")?;
let response = reqwest::Client::default()
.get(ws_url)
.header("Authorization", header_code)
.upgrade()
.send()
.await?;
let mut websocket = response.into_websocket().await?;
while let Some(token) = websocket.try_next().await? {
if let Message::Text(response) = token {
let response = serde_json::from_str::<CodeWebsocketResponse>(&response)
.map_err(|e| RemoteAccessError::UnparseableResponse(e.to_string()))?;
match response.response_type.as_str() {
"token" => {
let recieve_app = app.clone();
tauri::async_runtime::spawn_blocking(move || {
manual_recieve_handshake(recieve_app, response.value);
});
return Ok(());
}
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
}
}
}
Err(RemoteAccessError::HandshakeFailed(
"Failed to connect to websocket".to_string(),
))
};
let result = load().await;
if let Err(err) = result {
warn!("{err}");
app.emit("auth/failed", err.to_string()).unwrap();
}
});
Ok(code)
auth_initiate_logic()
}
#[tauri::command]

View File

@ -2,35 +2,30 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::warn;
use tauri::UriSchemeResponder;
use crate::{database::db::DatabaseImpls, remote::utils::DROP_CLIENT_ASYNC, DB};
use super::{
auth::generate_authorization_header,
cache::{ObjectCache, cache_object, get_cached_object},
cache::{cache_object, get_cached_object, ObjectCache},
requests::make_request,
};
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
// Drop leading /
let object_id = &request.uri().path()[1..];
let cache_result = get_cached_object::<ObjectCache>(object_id);
if let Ok(cache_result) = &cache_result
&& !cache_result.has_expired()
{
responder.respond(cache_result.into());
return;
}
let header = generate_authorization_header();
let client = DROP_CLIENT_ASYNC.clone();
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
let response = client.get(url).header("Authorization", header).send().await;
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
f.header("Authorization", header)
})
.unwrap()
.send();
if response.is_err() {
match cache_result {
Ok(cache_result) => responder.respond(cache_result.into()),
let data = get_cached_object::<&str, ObjectCache>(object_id);
match data {
Ok(data) => responder.respond(data.into()),
Err(e) => {
warn!("{e}");
warn!("{e}")
}
}
return;
@ -41,11 +36,18 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
CONTENT_TYPE,
response.headers().get("Content-Type").unwrap(),
);
let data = Vec::from(response.bytes().await.unwrap());
let data = Vec::from(response.bytes().unwrap());
let resp = resp_builder.body(data).unwrap();
if cache_result.is_err() || cache_result.unwrap().has_expired() {
cache_object::<ObjectCache>(object_id, &resp.clone().into()).unwrap();
}
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
responder.respond(resp);
}
pub fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
let object_id = &request.uri().path()[1..];
let data = get_cached_object::<&str, ObjectCache>(object_id);
match data {
Ok(data) => responder.respond(data.into()),
Err(e) => warn!("{e}"),
}
}

View File

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

View File

@ -1,9 +1,10 @@
use std::str::FromStr;
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
use reqwest::blocking::Client;
use tauri::UriSchemeResponder;
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC};
use crate::database::db::borrow_db_checked;
pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
let four_oh_four = Response::builder()
@ -32,12 +33,15 @@ pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeRespon
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
if whitelist_prefix
.iter()
.all(|f| !path.starts_with(f))
{
webbrowser::open(&new_uri.to_string()).unwrap();
return;
}
let client = DROP_CLIENT_SYNC.clone();
let client = Client::new();
let response = client
.request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {web_token}"))

View File

@ -1,18 +1,12 @@
use std::{
fs::{self, File},
io::Read,
sync::{LazyLock, Mutex},
};
use std::sync::Mutex;
use log::{debug, info, warn};
use reqwest::Certificate;
use log::{debug, warn};
use serde::Deserialize;
use url::Url;
use crate::{
AppState, AppStatus,
database::db::{DATA_ROOT_DIR, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError,
database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppState,
AppStatus,
};
#[derive(Deserialize)]
@ -21,60 +15,6 @@ struct DropHealthcheck {
app_name: String,
}
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
fn fetch_certificates() -> Vec<Certificate> {
let certificate_dir = DATA_ROOT_DIR.join("certificates");
let mut certs = Vec::new();
match fs::read_dir(certificate_dir) {
Ok(c) => {
for entry in c {
match entry {
Ok(c) => {
let mut buf = Vec::new();
File::open(c.path()).unwrap().read_to_end(&mut buf).unwrap();
for cert in Certificate::from_pem_bundle(&buf).unwrap() {
certs.push(cert);
}
info!(
"added {} certificate(s) from {}",
certs.len(),
c.file_name().into_string().unwrap()
);
}
Err(_) => todo!(),
}
}
}
Err(e) => {
debug!("not loading certificates due to error: {e}");
}
};
certs
}
pub fn get_client_sync() -> reqwest::blocking::Client {
let mut client = reqwest::blocking::ClientBuilder::new();
let certs = fetch_certificates();
for cert in certs {
client = client.add_root_certificate(cert);
}
client.build().unwrap()
}
pub fn get_client_async() -> reqwest::Client {
let mut client = reqwest::ClientBuilder::new();
let certs = fetch_certificates();
for cert in certs {
client = client.add_root_certificate(cert);
}
client.build().unwrap()
}
pub fn use_remote_logic(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
@ -84,8 +24,7 @@ pub fn use_remote_logic(
// Test Drop url
let test_endpoint = base_url.join("/api/v1")?;
let client = DROP_CLIENT_SYNC.clone();
let response = client.get(test_endpoint.to_string()).send()?;
let response = reqwest::blocking::get(test_endpoint.to_string())?;
let result: DropHealthcheck = response.json()?;

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.3.1-appimage",
"version": "0.3.0-rc-7",
"identifier": "dev.drop.app",
"build": {
"beforeDevCommand": "yarn dev --port 1432",
@ -11,7 +11,7 @@
},
"app": {
"security": {
"csp": "",
"csp": null,
"assetProtocol": {
"enable": true,
"scope": {}
@ -27,7 +27,7 @@
},
"bundle": {
"active": true,
"targets": ["nsis", "deb", "rpm", "dmg"],
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
"windows": {
"nsis": {
"installMode": "both"

View File

@ -1,5 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"exclude": ["src-tauri/**/*"]
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -54,13 +54,12 @@ export enum GameStatusEnum {
Remote = "Remote",
Queued = "Queued",
Downloading = "Downloading",
Validating = "Validating",
Installed = "Installed",
Updating = "Updating",
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
Running = "Running",
PartiallyInstalled = "PartiallyInstalled",
PartiallyInstalled = "PartiallyInstalled"
}
export type GameStatus = {

View File

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