mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-01-30 19:15:17 +01:00
Compare commits
59 Commits
async
...
beeth-appi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97083db78d | ||
|
|
ef2153611e | ||
|
|
48ccf638ce | ||
|
|
4361798661 | ||
|
|
732df1ddb9 | ||
|
|
4389d8beb8 | ||
|
|
38dd6a2c64 | ||
|
|
6038509a0c | ||
|
|
919148c6c2 | ||
|
|
b3f73d6d1e | ||
|
|
41d23730cc | ||
|
|
d19c5b8e97 | ||
|
|
c78079e4be | ||
|
|
92e42262c1 | ||
|
|
106c229c8f | ||
|
|
527476e7c1 | ||
|
|
81d324a026 | ||
|
|
3d5de9bec8 | ||
|
|
380084bb95 | ||
|
|
50d76bb76f | ||
|
|
de3be25914 | ||
|
|
95fc7d9605 | ||
|
|
f9c45053cc | ||
|
|
683e3ac7f2 | ||
|
|
fe586be658 | ||
|
|
aa034c21a9 | ||
|
|
50f7955734 | ||
|
|
73608ab85b | ||
|
|
1ed29aaa12 | ||
|
|
849dc26677 | ||
|
|
7db2e42006 | ||
|
|
61fa43dd86 | ||
|
|
301152a69f | ||
|
|
5e945734c7 | ||
|
|
53211e94fe | ||
|
|
af49a6751f | ||
|
|
9e93ec688c | ||
|
|
a70c2c64e4 | ||
|
|
d63d6efa3e | ||
|
|
51555e43ee | ||
|
|
cb7c297443 | ||
|
|
4484de0507 | ||
|
|
4599e5713c | ||
|
|
263cceb17f | ||
|
|
0249a3248f | ||
|
|
bf4b2e0ad6 | ||
|
|
7555cc3cdf | ||
|
|
6f30d248eb | ||
|
|
cc562662f0 | ||
|
|
dbe8c8df4d | ||
|
|
35f49b8811 | ||
|
|
cc5339a389 | ||
|
|
6104bfda72 | ||
|
|
be688cb18f | ||
|
|
13cc69f10e | ||
|
|
574782f445 | ||
|
|
b5a8543194 | ||
|
|
d0e4aea5ce | ||
|
|
739e6166c5 |
64
.github/workflows/release.yml
vendored
64
.github/workflows/release.yml
vendored
@@ -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: '--target aarch64-unknown-linux-gnu'
|
||||
args: ''
|
||||
- platform: 'windows-latest'
|
||||
args: ''
|
||||
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'yarn'
|
||||
|
||||
- name: install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
@@ -51,19 +52,74 @@ 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
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf binutils fuse
|
||||
# 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.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
- id: build
|
||||
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__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
tagName: v__VERSION__ # update the below "upload artifacts to GitHub" if this gets updated
|
||||
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
11
.gitignore
vendored
@@ -26,4 +26,13 @@ dist-ssr
|
||||
.output
|
||||
|
||||
src-tauri/flamegraph.svg
|
||||
src-tauri/perf*
|
||||
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
|
||||
3
app.vue
3
app.vue
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<LoadingIndicator />
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
@@ -22,11 +23,11 @@ const router = useRouter();
|
||||
const state = useAppState();
|
||||
try {
|
||||
state.value = JSON.parse(await invoke("fetch_state"));
|
||||
console.log(state.value)
|
||||
} catch (e) {
|
||||
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"));
|
||||
|
||||
1
build/appimage/drop-app.d/.DirIcon
Symbolic link
1
build/appimage/drop-app.d/.DirIcon
Symbolic link
@@ -0,0 +1 @@
|
||||
drop-oss-app.png
|
||||
8
build/appimage/drop-app.d/AppRun
Executable file
8
build/appimage/drop-app.d/AppRun
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/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
|
||||
8
build/appimage/drop-app.d/drop-oss-app.desktop
Normal file
8
build/appimage/drop-app.d/drop-oss-app.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[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
|
||||
BIN
build/appimage/drop-app.d/drop-oss-app.png
Normal file
BIN
build/appimage/drop-app.d/drop-oss-app.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
0
build/appimage/drop-app.d/usr/bin/.gitkeep
Normal file
0
build/appimage/drop-app.d/usr/bin/.gitkeep
Normal file
0
build/appimage/drop-app.d/usr/lib/.gitkeep
Normal file
0
build/appimage/drop-app.d/usr/lib/.gitkeep
Normal file
42
build_appimage.sh
Executable file
42
build_appimage.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/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/"
|
||||
@@ -1,52 +1,78 @@
|
||||
<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-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-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>
|
||||
<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>
|
||||
@@ -61,6 +87,7 @@ import {
|
||||
ChevronDownIcon,
|
||||
PlayIcon,
|
||||
QueueListIcon,
|
||||
ServerIcon,
|
||||
StopIcon,
|
||||
WrenchIcon,
|
||||
} from "@heroicons/vue/20/solid";
|
||||
@@ -78,7 +105,7 @@ const emit = defineEmits<{
|
||||
(e: "uninstall"): void;
|
||||
(e: "kill"): void;
|
||||
(e: "options"): void;
|
||||
(e: "resume"): void
|
||||
(e: "resume"): void;
|
||||
}>();
|
||||
|
||||
const showDropdown = computed(
|
||||
@@ -88,6 +115,10 @@ 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",
|
||||
@@ -95,6 +126,8 @@ const styles: { [key in GameStatusEnum]: string } = {
|
||||
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
|
||||
[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]:
|
||||
@@ -106,42 +139,45 @@ 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-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
|
||||
"bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-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.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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<PanelWidget class="flex-col gap-y-2">
|
||||
<div 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>
|
||||
</PanelWidget>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
|
||||
@@ -13,11 +13,7 @@
|
||||
<div class="max-w-lg">
|
||||
<slot />
|
||||
<div class="mt-10">
|
||||
<button
|
||||
@click="() => authWrapper_wrapper()"
|
||||
:disabled="loading"
|
||||
class="text-sm text-left font-semibold leading-7 text-blue-600"
|
||||
>
|
||||
<div>
|
||||
<div v-if="loading" role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -37,10 +33,19 @@
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span v-else>
|
||||
Sign in with your browser <span aria-hidden="true">→</span>
|
||||
<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 →
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5" v-if="offerManual">
|
||||
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
|
||||
@@ -121,6 +126,7 @@
|
||||
|
||||
<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);
|
||||
|
||||
@@ -81,18 +81,20 @@ 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-600",
|
||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||
};
|
||||
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...",
|
||||
|
||||
7
components/LoadingIndicator.vue
Normal file
7
components/LoadingIndicator.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const loading = useLoadingIndicator();
|
||||
|
||||
watch(loading.isLoading, console.log);
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
|
||||
class="h-16 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" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "drop-app",
|
||||
"private": true,
|
||||
"version": "0.3.0-rc-8",
|
||||
"version": "0.3.1-appimage",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
|
||||
37
pages/auth/code.vue
Normal file
37
pages/auth/code.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<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">←</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>
|
||||
@@ -76,9 +76,8 @@
|
||||
<TransitionGroup name="slide" tag="div" class="h-full">
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
:key="url"
|
||||
:key="index"
|
||||
:src="url"
|
||||
loading="lazy"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
v-show="index === currentImageIndex"
|
||||
/>
|
||||
@@ -158,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">
|
||||
@@ -351,9 +350,7 @@
|
||||
<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"
|
||||
@@ -371,7 +368,18 @@
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
|
||||
<GameOptionsModal v-if="status.type === GameStatusEnum.Installed" v-model="configureModalOpen" :game-id="game.id" />
|
||||
<!--
|
||||
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"
|
||||
/>
|
||||
|
||||
<Transition
|
||||
enter="transition ease-out duration-300"
|
||||
@@ -421,7 +429,7 @@
|
||||
<img
|
||||
v-for="(url, index) in mediaUrls"
|
||||
v-show="currentImageIndex === index"
|
||||
:key="url"
|
||||
:key="index"
|
||||
:src="url"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
:alt="`${game.mName} screenshot ${index + 1}`"
|
||||
@@ -442,10 +450,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxLabel,
|
||||
@@ -483,7 +487,10 @@ const bannerUrl = await useObject(game.value.mBannerObjectId);
|
||||
|
||||
// Get all available images
|
||||
const mediaUrls = await Promise.all(
|
||||
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
|
||||
game.value.mImageCarouselObjectIds.map(async (v) => {
|
||||
const src = await useObject(v);
|
||||
return src;
|
||||
})
|
||||
);
|
||||
|
||||
const htmlDescription = micromark(game.value.mDescription);
|
||||
@@ -497,7 +504,6 @@ const currentImageIndex = ref(0);
|
||||
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
|
||||
async function installFlow() {
|
||||
installFlowOpen.value = true;
|
||||
versionOptions.value = undefined;
|
||||
@@ -536,12 +542,11 @@ 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() {
|
||||
|
||||
1237
src-tauri/Cargo.lock
generated
1237
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "drop-app"
|
||||
version = "0.3.0-rc-8"
|
||||
version = "0.3.1-appimage"
|
||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||
authors = ["Drop OSS"]
|
||||
edition = "2024"
|
||||
@@ -25,6 +25,7 @@ tauri-build = { version = "2.0.0", features = [] }
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
serde_json = "1"
|
||||
rayon = "1.10.0"
|
||||
webbrowser = "1.0.2"
|
||||
url = "2.5.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
@@ -67,11 +68,9 @@ 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"
|
||||
async-trait = "0.1.88"
|
||||
futures = "0.3.31"
|
||||
tokio-util = { version = "0.7.15", features = ["io"] }
|
||||
async-scoped = { version = "0.9.0", features = ["use-tokio"] }
|
||||
async-once-cell = "0.5.4"
|
||||
reqwest-websocket = "0.5.0"
|
||||
futures-lite = "2.6.0"
|
||||
page_size = "0.6.0"
|
||||
# tailscale = { path = "./tailscale" }
|
||||
|
||||
[dependencies.dynfmt]
|
||||
@@ -98,14 +97,14 @@ features = ["fs"]
|
||||
version = "1.10.0"
|
||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||
|
||||
[dependencies.dropbreak]
|
||||
git = "https://github.com/Drop-OSS/dropbreak.git"
|
||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||
[dependencies.rustbreak]
|
||||
version = "2"
|
||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
default-features = false
|
||||
features = ["json", "http2", "rustls-tls-webpki-roots", "stream"]
|
||||
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use log::debug;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
|
||||
pub async fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
let manager = app.autolaunch();
|
||||
if enabled {
|
||||
manager.enable().map_err(|e| e.to_string())?;
|
||||
@@ -14,18 +14,16 @@ pub async fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(),
|
||||
}
|
||||
|
||||
// Store the state in DB
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_autostart_enabled_logic(
|
||||
app: AppHandle,
|
||||
) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
// First check DB state
|
||||
let db_handle = borrow_db_checked().await;
|
||||
let db_handle = borrow_db_checked();
|
||||
let db_state = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
@@ -46,8 +44,8 @@ pub async fn get_autostart_enabled_logic(
|
||||
}
|
||||
|
||||
// New function to sync state on startup
|
||||
pub async fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
let db_handle = borrow_db_checked().await;
|
||||
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
let db_handle = borrow_db_checked();
|
||||
let should_be_enabled = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
@@ -67,11 +65,11 @@ pub async fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
toggle_autostart_logic(app, enabled).await
|
||||
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
|
||||
toggle_autostart_logic(app, enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
get_autostart_enabled_logic(app).await
|
||||
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
get_autostart_enabled_logic(app)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
use log::{debug, error};
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::DropFunctionState;
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn quit<>(app: tauri::AppHandle, state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
||||
cleanup_and_exit(&app, &state).await;
|
||||
|
||||
Ok(())
|
||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
cleanup_and_exit(&app, &state);
|
||||
}
|
||||
|
||||
pub async fn cleanup_and_exit(
|
||||
app: &AppHandle,
|
||||
state: &tauri::State<'_, DropFunctionState<'_>>,
|
||||
) {
|
||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||
debug!("cleaning up and exiting application");
|
||||
let download_manager = state.lock().await.download_manager.clone();
|
||||
match download_manager.ensure_terminated().await {
|
||||
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:?}"),
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::DropFunctionState;
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_state(
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
pub fn fetch_state(
|
||||
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||
) -> Result<String, String> {
|
||||
let guard = state.lock().await;
|
||||
let guard = state.lock().unwrap();
|
||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||
drop(guard);
|
||||
Ok(cloned_state)
|
||||
|
||||
@@ -7,11 +7,11 @@ use std::{
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
|
||||
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
|
||||
};
|
||||
|
||||
use super::{
|
||||
db::{DATA_ROOT_DIR, borrow_db_checked},
|
||||
db::{borrow_db_checked, DATA_ROOT_DIR},
|
||||
debug::SystemData,
|
||||
models::data::Settings,
|
||||
};
|
||||
@@ -19,19 +19,19 @@ use super::{
|
||||
// Will, in future, return disk/remaining size
|
||||
// Just returns the directories that have been set up
|
||||
#[tauri::command]
|
||||
pub async fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||
let lock = borrow_db_checked().await;
|
||||
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||
let lock = borrow_db_checked();
|
||||
lock.applications.install_dirs.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_download_dir(index: usize) {
|
||||
let mut lock = borrow_db_mut_checked().await;
|
||||
pub fn delete_download_dir(index: usize) {
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
lock.applications.install_dirs.remove(index);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
||||
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
||||
// Check the new directory is all good
|
||||
let new_dir_path = Path::new(&new_dir);
|
||||
if new_dir_path.exists() {
|
||||
@@ -48,7 +48,7 @@ pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerErr
|
||||
}
|
||||
|
||||
// Add it to the dictionary
|
||||
let mut lock = borrow_db_mut_checked().await;
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
if lock.applications.install_dirs.contains(&new_dir) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::AlreadyExists,
|
||||
@@ -59,12 +59,14 @@ pub async fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerErr
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
|
||||
scan_install_dirs();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_settings(new_settings: Value) {
|
||||
let mut db_lock = borrow_db_mut_checked().await;
|
||||
pub fn update_settings(new_settings: Value) {
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
||||
for (key, value) in new_settings.as_object().unwrap() {
|
||||
current_settings[key] = value.clone();
|
||||
@@ -73,12 +75,12 @@ pub async fn update_settings(new_settings: Value) {
|
||||
db_lock.settings = new_settings;
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn fetch_settings() -> Settings {
|
||||
borrow_db_checked().await.settings.clone()
|
||||
pub fn fetch_settings() -> Settings {
|
||||
borrow_db_checked().settings.clone()
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn fetch_system_data() -> SystemData {
|
||||
let db_handle = borrow_db_checked().await;
|
||||
pub fn fetch_system_data() -> SystemData {
|
||||
let db_handle = borrow_db_checked();
|
||||
SystemData::new(
|
||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
db_handle.base_url.clone(),
|
||||
|
||||
@@ -3,19 +3,14 @@ use std::{
|
||||
mem::ManuallyDrop,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
sync::{Arc, LazyLock},
|
||||
sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use async_once_cell::OnceCell;
|
||||
use chrono::Utc;
|
||||
use dropbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use log::{debug, info, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
use native_model::{Decode, Encode};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use tokio::{
|
||||
spawn,
|
||||
sync::{RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::DB;
|
||||
@@ -32,15 +27,15 @@ pub struct DropDatabaseSerializer;
|
||||
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||
for DropDatabaseSerializer
|
||||
{
|
||||
fn serialize(&self, val: &T) -> dropbreak::error::DeSerResult<Vec<u8>> {
|
||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
||||
.map_err(|e| DeSerError::Internal(e.to_string()))
|
||||
}
|
||||
|
||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> dropbreak::error::DeSerResult<T> {
|
||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||
let mut buf = Vec::new();
|
||||
s.read_to_end(&mut buf)
|
||||
.map_err(|e| dropbreak::error::DeSerError::Other(e.into()))?;
|
||||
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
||||
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||
Ok(val)
|
||||
@@ -48,33 +43,15 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||
}
|
||||
|
||||
pub type DatabaseInterface =
|
||||
dropbreak::Database<Database, dropbreak::backend::PathBackend, DropDatabaseSerializer>;
|
||||
|
||||
pub struct OnceCellDatabase(OnceCell<DatabaseInterface>);
|
||||
impl OnceCellDatabase {
|
||||
pub const fn new() -> Self {
|
||||
Self(OnceCell::new())
|
||||
}
|
||||
|
||||
pub async fn init(&self, init: impl Future<Output = DatabaseInterface>) {
|
||||
self.0.get_or_init(init).await;
|
||||
}
|
||||
}
|
||||
impl<'a> Deref for OnceCellDatabase {
|
||||
type Target = DatabaseInterface;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.get().unwrap()
|
||||
}
|
||||
}
|
||||
rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer>;
|
||||
|
||||
pub trait DatabaseImpls {
|
||||
async fn set_up_database() -> DatabaseInterface;
|
||||
async fn database_is_set_up(&self) -> bool;
|
||||
async fn fetch_base_url(&self) -> Url;
|
||||
fn set_up_database() -> DatabaseInterface;
|
||||
fn database_is_set_up(&self) -> bool;
|
||||
fn fetch_base_url(&self) -> Url;
|
||||
}
|
||||
impl DatabaseImpls for DatabaseInterface {
|
||||
async fn set_up_database() -> DatabaseInterface {
|
||||
fn set_up_database() -> DatabaseInterface {
|
||||
let db_path = DATA_ROOT_DIR.join("drop.db");
|
||||
let games_base_dir = DATA_ROOT_DIR.join("games");
|
||||
let logs_root_dir = DATA_ROOT_DIR.join("logs");
|
||||
@@ -90,40 +67,38 @@ impl DatabaseImpls for DatabaseInterface {
|
||||
|
||||
let exists = fs::exists(db_path.clone()).unwrap();
|
||||
|
||||
match exists {
|
||||
true => match PathDatabase::load_from_path(db_path.clone()).await {
|
||||
if exists {
|
||||
match PathDatabase::load_from_path(db_path.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir).await,
|
||||
},
|
||||
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)
|
||||
.await
|
||||
.expect("Database could not be created")
|
||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||
}
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
|
||||
async fn database_is_set_up(&self) -> bool {
|
||||
!self.borrow_data().await.base_url.is_empty()
|
||||
fn database_is_set_up(&self) -> bool {
|
||||
!self.borrow_data().unwrap().base_url.is_empty()
|
||||
}
|
||||
|
||||
async fn fetch_base_url(&self) -> Url {
|
||||
let handle = self.borrow_data().await;
|
||||
fn fetch_base_url(&self) -> Url {
|
||||
let handle = self.borrow_data().unwrap();
|
||||
Url::parse(&handle.base_url).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_invalid_database(
|
||||
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||
fn handle_invalid_database(
|
||||
_e: RustbreakError,
|
||||
db_path: PathBuf,
|
||||
games_base_dir: PathBuf,
|
||||
cache_dir: PathBuf,
|
||||
) -> dropbreak::Database<Database, dropbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
|
||||
warn!("{_e}");
|
||||
let new_path = {
|
||||
let time = Utc::now().timestamp();
|
||||
@@ -140,23 +115,14 @@ async fn handle_invalid_database(
|
||||
cache_dir,
|
||||
);
|
||||
|
||||
PathDatabase::create_at_path(db_path, db)
|
||||
.await
|
||||
.expect("Database could not be created")
|
||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||
}
|
||||
|
||||
// To automatically save the database upon drop
|
||||
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
||||
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||
impl<'a> Deref for DBWrite<'a> {
|
||||
type Target = RwLockWriteGuard<'a, Database>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Deref for DBRead<'a> {
|
||||
type Target = RwLockReadGuard<'a, Database>;
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@@ -167,26 +133,45 @@ impl<'a> DerefMut for DBWrite<'a> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl<'a> Drop for DBWrite<'a> {
|
||||
impl<'a> Deref for DBRead<'a> {
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl Drop for DBWrite<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.0);
|
||||
}
|
||||
|
||||
spawn(async {
|
||||
match DB.save().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
panic!("database failed to save with error {e}")
|
||||
}
|
||||
match DB.save() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
error!("database failed to save with error {e}");
|
||||
panic!("database failed to save with error {e}")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||
DBRead(DB.borrow_data().await)
|
||||
|
||||
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => DBRead(data),
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {e}");
|
||||
panic!("database borrow failed with error {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
||||
DBWrite(ManuallyDrop::new(DB.borrow_data_mut().await))
|
||||
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => DBWrite(ManuallyDrop::new(data)),
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {e}");
|
||||
panic!("database borrow mut failed with error {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod commands;
|
||||
pub mod db;
|
||||
pub mod debug;
|
||||
pub mod models;
|
||||
pub mod models;
|
||||
pub mod scan;
|
||||
@@ -1,8 +1,14 @@
|
||||
use crate::database::models::data::Database;
|
||||
|
||||
/**
|
||||
* NEXT BREAKING CHANGE
|
||||
*
|
||||
* UPDATE DATABASE TO USE RPMSERDENAMED
|
||||
*
|
||||
* WE CAN'T DELETE ANY FIELDS
|
||||
*/
|
||||
pub mod data {
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
use native_model::native_model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -18,16 +24,14 @@ pub mod data {
|
||||
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod v1 {
|
||||
use crate::process::process_manager::Platform;
|
||||
use serde_with::serde_as;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use super::*;
|
||||
use super::{Deserialize, Serialize, native_model};
|
||||
|
||||
fn default_template() -> String {
|
||||
"{}".to_owned()
|
||||
@@ -115,6 +119,7 @@ pub mod data {
|
||||
Downloading { version_name: String },
|
||||
Uninstalling {},
|
||||
Updating { version_name: String },
|
||||
Validating { version_name: String },
|
||||
Running {},
|
||||
}
|
||||
|
||||
@@ -174,7 +179,10 @@ pub mod data {
|
||||
|
||||
use serde_with::serde_as;
|
||||
|
||||
use super::*;
|
||||
use super::{
|
||||
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
|
||||
GameVersion, Serialize, Settings, native_model, v1,
|
||||
};
|
||||
|
||||
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
@@ -206,7 +214,7 @@ pub mod data {
|
||||
applications: value.applications,
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: crate::database::models::Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,7 +291,10 @@ pub mod data {
|
||||
mod v3 {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use super::{
|
||||
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
|
||||
Settings, native_model, v2,
|
||||
};
|
||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct Database {
|
||||
@@ -297,6 +308,7 @@ 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 {
|
||||
@@ -306,22 +318,13 @@ pub mod data {
|
||||
applications: value.applications.into(),
|
||||
prev_database: value.prev_database,
|
||||
cache_dir: value.cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
@@ -336,11 +339,11 @@ pub mod data {
|
||||
transient_statuses: HashMap::new(),
|
||||
},
|
||||
prev_database,
|
||||
base_url: "".to_owned(),
|
||||
base_url: String::new(),
|
||||
auth: None,
|
||||
settings: Settings::default(),
|
||||
cache_dir,
|
||||
compat_info: Database::create_new_compat_info(),
|
||||
compat_info: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
src-tauri/src/database/scan.rs
Normal file
52
src-tauri/src/database/scan.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,31 @@
|
||||
use crate::{database::models::data::DownloadableMetadata, DropFunctionState};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn pause_downloads(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
||||
state.lock().await.download_manager.pause_downloads();
|
||||
Ok(())
|
||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.pause_downloads();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn resume_downloads(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
||||
state.lock().await.download_manager.resume_downloads();
|
||||
Ok(())
|
||||
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
state.lock().unwrap().download_manager.resume_downloads();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn move_download_in_queue(
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
pub fn move_download_in_queue(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
old_index: usize,
|
||||
new_index: usize,
|
||||
) -> Result<(), ()> {
|
||||
) {
|
||||
state
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.rearrange(old_index, new_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn cancel_game(state: tauri::State<'_, DropFunctionState<'_>>, meta: DownloadableMetadata) -> Result<(), ()> {
|
||||
state.lock().await.download_manager.cancel(meta);
|
||||
Ok(())
|
||||
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
|
||||
state.lock().unwrap().download_manager.cancel(meta);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
Arc,
|
||||
Arc, Mutex,
|
||||
mpsc::{Receiver, Sender, channel},
|
||||
},
|
||||
thread::{JoinHandle, spawn},
|
||||
};
|
||||
|
||||
use ::futures::future::join_all;
|
||||
use log::{debug, error, info, warn};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::{runtime::Runtime, sync::Mutex};
|
||||
|
||||
use crate::{
|
||||
database::models::data::DownloadableMetadata,
|
||||
@@ -70,15 +69,14 @@ Behold, my madness - quexeky
|
||||
pub struct DownloadManagerBuilder {
|
||||
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
||||
download_queue: Queue,
|
||||
command_receiver: Mutex<Receiver<DownloadManagerSignal>>,
|
||||
command_receiver: Receiver<DownloadManagerSignal>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
progress: CurrentProgressObject,
|
||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||
app_handle: AppHandle,
|
||||
runtime: Runtime,
|
||||
|
||||
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
||||
current_download_thread: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||
active_control_flag: Option<DownloadThreadControl>,
|
||||
}
|
||||
impl DownloadManagerBuilder {
|
||||
@@ -91,110 +89,97 @@ impl DownloadManagerBuilder {
|
||||
let manager = Self {
|
||||
download_agent_registry: HashMap::new(),
|
||||
download_queue: queue.clone(),
|
||||
command_receiver: Mutex::new(command_receiver),
|
||||
command_receiver,
|
||||
status: status.clone(),
|
||||
sender: command_sender.clone(),
|
||||
progress: active_progress.clone(),
|
||||
app_handle,
|
||||
runtime: tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap(),
|
||||
|
||||
current_download_agent: None,
|
||||
current_download_thread: Mutex::new(None),
|
||||
active_control_flag: None,
|
||||
};
|
||||
|
||||
let terminator = tauri::async_runtime::spawn(async {
|
||||
if let Err(_err) = manager.manage_queue().await {
|
||||
panic!("download manager exited with error");
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
let terminator = spawn(|| manager.manage_queue());
|
||||
|
||||
DownloadManager::new(terminator, queue, active_progress, command_sender)
|
||||
}
|
||||
|
||||
async fn set_status(&self, status: DownloadManagerStatus) {
|
||||
*self.status.lock().await = status;
|
||||
fn set_status(&self, status: DownloadManagerStatus) {
|
||||
*self.status.lock().unwrap() = status;
|
||||
}
|
||||
|
||||
async fn remove_and_cleanup_front_download(
|
||||
&mut self,
|
||||
meta: &DownloadableMetadata,
|
||||
) -> DownloadAgent {
|
||||
fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
|
||||
self.download_queue.pop_front();
|
||||
let download_agent = self.download_agent_registry.remove(meta).unwrap();
|
||||
self.cleanup_current_download().await;
|
||||
self.cleanup_current_download();
|
||||
download_agent
|
||||
}
|
||||
|
||||
// CAREFUL WITH THIS FUNCTION
|
||||
// Make sure the download thread is terminated
|
||||
async fn cleanup_current_download(&mut self) {
|
||||
fn cleanup_current_download(&mut self) {
|
||||
self.active_control_flag = None;
|
||||
*self.progress.lock().await = None;
|
||||
*self.progress.lock().unwrap() = None;
|
||||
self.current_download_agent = None;
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
*download_thread_lock = None;
|
||||
drop(download_thread_lock);
|
||||
}
|
||||
|
||||
async fn stop_and_wait_current_download(&self) {
|
||||
self.set_status(DownloadManagerStatus::Paused).await;
|
||||
fn stop_and_wait_current_download(&self) {
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
if let Some(current_flag) = &self.active_control_flag {
|
||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||
current_download_thread.await.unwrap();
|
||||
current_download_thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
async fn manage_queue(mut self) -> Result<(), ()> {
|
||||
fn manage_queue(mut self) -> Result<(), ()> {
|
||||
loop {
|
||||
let signal = match self.command_receiver.lock().await.recv() {
|
||||
let signal = match self.command_receiver.recv() {
|
||||
Ok(signal) => signal,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
match signal {
|
||||
DownloadManagerSignal::Go => {
|
||||
self.manage_go_signal().await;
|
||||
self.manage_go_signal();
|
||||
}
|
||||
DownloadManagerSignal::Stop => {
|
||||
self.manage_stop_signal().await;
|
||||
self.manage_stop_signal();
|
||||
}
|
||||
DownloadManagerSignal::Completed(meta) => {
|
||||
self.manage_completed_signal(meta).await;
|
||||
self.manage_completed_signal(meta);
|
||||
}
|
||||
DownloadManagerSignal::Queue(download_agent) => {
|
||||
self.manage_queue_signal(download_agent).await;
|
||||
self.manage_queue_signal(download_agent);
|
||||
}
|
||||
DownloadManagerSignal::Error(e) => {
|
||||
self.manage_error_signal(e).await;
|
||||
self.manage_error_signal(e);
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIQueue => {
|
||||
self.push_ui_queue_update().await;
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
DownloadManagerSignal::UpdateUIStats(kbs, time) => {
|
||||
self.push_ui_stats_update(kbs, time);
|
||||
}
|
||||
DownloadManagerSignal::Finish => {
|
||||
self.stop_and_wait_current_download().await;
|
||||
self.stop_and_wait_current_download();
|
||||
return Ok(());
|
||||
}
|
||||
DownloadManagerSignal::Cancel(meta) => {
|
||||
self.manage_cancel_signal(&meta).await;
|
||||
self.manage_cancel_signal(&meta);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
|
||||
debug!("got signal Queue");
|
||||
let meta = download_agent.metadata();
|
||||
|
||||
@@ -205,7 +190,7 @@ impl DownloadManagerBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
download_agent.on_initialised(&self.app_handle).await;
|
||||
download_agent.on_initialised(&self.app_handle);
|
||||
self.download_queue.append(meta.clone());
|
||||
self.download_agent_registry.insert(meta, download_agent);
|
||||
|
||||
@@ -214,7 +199,7 @@ impl DownloadManagerBuilder {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn manage_go_signal(&mut self) {
|
||||
fn manage_go_signal(&mut self) {
|
||||
debug!("got signal Go");
|
||||
if self.download_agent_registry.is_empty() {
|
||||
debug!(
|
||||
@@ -248,117 +233,110 @@ impl DownloadManagerBuilder {
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
self.active_control_flag = Some(download_agent.control_flag().await);
|
||||
self.active_control_flag = Some(download_agent.control_flag());
|
||||
self.current_download_agent = Some(download_agent.clone());
|
||||
|
||||
info!("fetched control flag");
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
info!("cloned sender");
|
||||
|
||||
let mut download_thread_lock = self.current_download_thread.lock().await;
|
||||
info!("acquired download thread lock");
|
||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||
let app_handle = self.app_handle.clone();
|
||||
|
||||
info!("starting download agent thread");
|
||||
|
||||
*download_thread_lock = Some(self.runtime.spawn(async move {
|
||||
info!("started download agent thread");
|
||||
match download_agent.download(&app_handle).await {
|
||||
// Ok(true) is for completed and exited properly
|
||||
Ok(true) => {
|
||||
debug!("download {:?} has completed", download_agent.metadata());
|
||||
match download_agent.validate().await {
|
||||
Ok(true) => {
|
||||
download_agent.on_complete(&app_handle).await;
|
||||
sender
|
||||
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
|
||||
.unwrap();
|
||||
}
|
||||
Ok(false) => {
|
||||
download_agent.on_incomplete(&app_handle).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"download {:?} has validation error {}",
|
||||
download_agent.metadata(),
|
||||
&e
|
||||
);
|
||||
download_agent.on_error(&app_handle, &e).await;
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
*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;
|
||||
}
|
||||
};
|
||||
|
||||
// If the download gets cancelled
|
||||
// immediately return, on_cancelled gets called for us earlier
|
||||
if !download_result {
|
||||
return;
|
||||
}
|
||||
// Ok(false) is for incomplete but exited properly
|
||||
Ok(false) => {
|
||||
debug!("Donwload agent finished incomplete");
|
||||
download_agent.on_incomplete(&app_handle).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("download {:?} has error {}", download_agent.metadata(), &e);
|
||||
download_agent.on_error(&app_handle, &e).await;
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
|
||||
}));
|
||||
|
||||
self.set_status(DownloadManagerStatus::Downloading).await;
|
||||
self.set_status(DownloadManagerStatus::Downloading);
|
||||
let active_control_flag = self.active_control_flag.clone().unwrap();
|
||||
active_control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
// download_thread_lock.take().unwrap().await;
|
||||
}
|
||||
async fn manage_stop_signal(&mut self) {
|
||||
fn manage_stop_signal(&mut self) {
|
||||
debug!("got signal Stop");
|
||||
|
||||
if let Some(active_control_flag) = self.active_control_flag.clone() {
|
||||
self.set_status(DownloadManagerStatus::Paused).await;
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
active_control_flag.set(DownloadThreadControlFlag::Stop);
|
||||
}
|
||||
}
|
||||
async fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||
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).await;
|
||||
self.remove_and_cleanup_front_download(&meta);
|
||||
}
|
||||
|
||||
self.push_ui_queue_update().await;
|
||||
self.push_ui_queue_update();
|
||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
async fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||
debug!("got signal Error");
|
||||
if let Some(current_agent) = self.current_download_agent.clone() {
|
||||
current_agent.on_error(&self.app_handle, &error).await;
|
||||
current_agent.on_error(&self.app_handle, &error);
|
||||
|
||||
self.stop_and_wait_current_download().await;
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata())
|
||||
.await;
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
}
|
||||
self.set_status(DownloadManagerStatus::Error).await;
|
||||
self.set_status(DownloadManagerStatus::Error);
|
||||
}
|
||||
async fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
debug!("got signal Cancel");
|
||||
|
||||
if let Some(current_download) = &self.current_download_agent {
|
||||
if ¤t_download.metadata() == meta {
|
||||
self.set_status(DownloadManagerStatus::Paused).await;
|
||||
current_download.on_cancelled(&self.app_handle).await;
|
||||
self.stop_and_wait_current_download().await;
|
||||
self.set_status(DownloadManagerStatus::Paused);
|
||||
current_download.on_cancelled(&self.app_handle);
|
||||
self.stop_and_wait_current_download();
|
||||
|
||||
self.download_queue.pop_front();
|
||||
|
||||
self.cleanup_current_download().await;
|
||||
self.cleanup_current_download();
|
||||
debug!("current download queue: {:?}", self.download_queue.read());
|
||||
}
|
||||
// TODO: Collapse these two into a single if statement somehow
|
||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle).await;
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
debug!(
|
||||
@@ -371,7 +349,7 @@ impl DownloadManagerBuilder {
|
||||
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||
let index = self.download_queue.get_by_meta(meta);
|
||||
if let Some(index) = index {
|
||||
download_agent.on_cancelled(&self.app_handle).await;
|
||||
download_agent.on_cancelled(&self.app_handle);
|
||||
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||
let removed = self.download_agent_registry.remove(meta);
|
||||
debug!(
|
||||
@@ -381,26 +359,28 @@ impl DownloadManagerBuilder {
|
||||
);
|
||||
}
|
||||
}
|
||||
self.push_ui_queue_update().await;
|
||||
self.push_ui_queue_update();
|
||||
}
|
||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||
let event_data = StatsUpdateEvent { speed: kbs, time };
|
||||
|
||||
self.app_handle.emit("update_stats", event_data).unwrap();
|
||||
}
|
||||
async fn push_ui_queue_update(&self) {
|
||||
fn push_ui_queue_update(&self) {
|
||||
let queue = &self.download_queue.read();
|
||||
let queue_objs = join_all(queue.iter().map(async |key| {
|
||||
let val = self.download_agent_registry.get(key).unwrap();
|
||||
QueueUpdateEventQueueData {
|
||||
meta: DownloadableMetadata::clone(key),
|
||||
status: val.status().await,
|
||||
progress: val.progress().await.get_progress(),
|
||||
current: val.progress().await.sum(),
|
||||
max: val.progress().await.get_max(),
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
let queue_objs = queue
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let val = self.download_agent_registry.get(key).unwrap();
|
||||
QueueUpdateEventQueueData {
|
||||
meta: DownloadableMetadata::clone(key),
|
||||
status: val.status(),
|
||||
progress: val.progress().get_progress(),
|
||||
current: val.progress().sum(),
|
||||
max: val.progress().get_max(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let event_data = QueueUpdateEvent { queue: queue_objs };
|
||||
self.app_handle.emit("update_queue", event_data).unwrap();
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::VecDeque,
|
||||
fmt::Debug,
|
||||
sync::{
|
||||
mpsc::{SendError, Sender},
|
||||
Mutex, MutexGuard,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use log::{debug, info};
|
||||
use serde::Serialize;
|
||||
use tauri::async_runtime::JoinHandle;
|
||||
|
||||
use crate::{
|
||||
database::models::data::DownloadableMetadata,
|
||||
@@ -22,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
|
||||
@@ -69,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 {
|
||||
@@ -117,8 +118,8 @@ impl DownloadManager {
|
||||
pub fn read_queue(&self) -> VecDeque<DownloadableMetadata> {
|
||||
self.download_queue.read()
|
||||
}
|
||||
pub async fn get_current_download_progress(&self) -> Option<f64> {
|
||||
let progress_object = (*self.progress.lock().await).clone()?;
|
||||
pub fn get_current_download_progress(&self) -> Option<f64> {
|
||||
let progress_object = (*self.progress.lock().unwrap()).clone()?;
|
||||
Some(progress_object.get_progress())
|
||||
}
|
||||
pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
|
||||
@@ -138,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 {
|
||||
@@ -170,19 +171,19 @@ impl DownloadManager {
|
||||
pub fn resume_downloads(&self) {
|
||||
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
||||
}
|
||||
pub async fn ensure_terminated(&self) -> Result<Result<(), ()>, tauri::Error> {
|
||||
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
|
||||
self.command_sender
|
||||
.send(DownloadManagerSignal::Finish)
|
||||
.unwrap();
|
||||
let terminator = self.terminator.lock().unwrap().take();
|
||||
terminator.unwrap().await
|
||||
terminator.unwrap().join()
|
||||
}
|
||||
pub fn get_sender(&self) -> Sender<DownloadManagerSignal> {
|
||||
self.command_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>>,
|
||||
|
||||
@@ -12,17 +12,16 @@ use super::{
|
||||
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Downloadable: Send + Sync {
|
||||
async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||
async fn progress(&self) -> Arc<ProgressObject>;
|
||||
async fn control_flag(&self) -> DownloadThreadControl;
|
||||
async fn validate(&self) -> Result<bool, ApplicationDownloadError>;
|
||||
async fn status(&self) -> DownloadStatus;
|
||||
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 status(&self) -> DownloadStatus;
|
||||
fn metadata(&self) -> DownloadableMetadata;
|
||||
async fn on_initialised(&self, app_handle: &AppHandle);
|
||||
async fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
||||
async fn on_complete(&self, app_handle: &AppHandle);
|
||||
async fn on_incomplete(&self, app_handle: &AppHandle);
|
||||
async fn on_cancelled(&self, app_handle: &AppHandle);
|
||||
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_cancelled(&self, app_handle: &AppHandle);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ impl From<DownloadThreadControlFlag> for bool {
|
||||
/// false => Stop
|
||||
impl From<bool> for DownloadThreadControlFlag {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => DownloadThreadControlFlag::Go,
|
||||
false => DownloadThreadControlFlag::Stop,
|
||||
}
|
||||
if value { DownloadThreadControlFlag::Go } else { DownloadThreadControlFlag::Stop }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +38,9 @@ impl DownloadThreadControl {
|
||||
}
|
||||
}
|
||||
pub fn get(&self) -> DownloadThreadControlFlag {
|
||||
self.inner.load(Ordering::Relaxed).into()
|
||||
self.inner.load(Ordering::Acquire).into()
|
||||
}
|
||||
pub fn set(&self, flag: DownloadThreadControlFlag) {
|
||||
self.inner.store(flag.into(), Ordering::Relaxed);
|
||||
self.inner.store(flag.into(), Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,20 +39,20 @@ impl ProgressHandle {
|
||||
}
|
||||
}
|
||||
pub fn set(&self, amount: usize) {
|
||||
self.progress.store(amount, Ordering::Relaxed);
|
||||
self.progress.store(amount, Ordering::Release);
|
||||
}
|
||||
pub fn add(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::AcqRel);
|
||||
calculate_update(&self.progress_object);
|
||||
}
|
||||
pub fn skip(&self, amount: usize) {
|
||||
self.progress
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
|
||||
.fetch_add(amount, std::sync::atomic::Ordering::Acquire);
|
||||
// Offset the bytes at last offset by this amount
|
||||
self.progress_object
|
||||
.bytes_last_update
|
||||
.fetch_add(amount, Ordering::Relaxed);
|
||||
.fetch_add(amount, Ordering::Acquire);
|
||||
// Dont' fire update
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,6 @@ 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),
|
||||
@@ -81,19 +80,18 @@ impl ProgressObject {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|instance| instance.load(Ordering::Relaxed))
|
||||
.map(|instance| instance.load(Ordering::Acquire))
|
||||
.sum()
|
||||
}
|
||||
pub fn reset(&self, size: usize) {
|
||||
pub fn reset(&self) {
|
||||
self.set_time_now();
|
||||
self.set_size(size);
|
||||
self.bytes_last_update.store(0, Ordering::Release);
|
||||
self.rolling.reset();
|
||||
self.progress_instances
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.for_each(|x| x.store(0, Ordering::Release));
|
||||
.for_each(|x| x.store(0, Ordering::SeqCst));
|
||||
}
|
||||
pub fn get_max(&self) -> usize {
|
||||
*self.max.lock().unwrap()
|
||||
@@ -127,7 +125,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::Relaxed);
|
||||
.swap(current_bytes_downloaded, Ordering::Acquire);
|
||||
|
||||
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
|
||||
|
||||
|
||||
@@ -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 < ¤t)
|
||||
.map(|(_, x)| x.load(Ordering::Relaxed))
|
||||
.map(|(_, x)| x.load(Ordering::Acquire))
|
||||
.sum::<usize>()
|
||||
/ S
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(SerializeDisplay)]
|
||||
pub enum ProcessError {
|
||||
SetupRequired,
|
||||
NotInstalled,
|
||||
AlreadyRunning,
|
||||
NotDownloaded,
|
||||
InvalidID,
|
||||
InvalidVersion,
|
||||
IOError(Error),
|
||||
@@ -19,10 +17,8 @@ pub enum ProcessError {
|
||||
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::NotDownloaded => "Game not downloaded",
|
||||
ProcessError::InvalidID => "Invalid game ID",
|
||||
ProcessError::InvalidVersion => "Invalid game version",
|
||||
ProcessError::IOError(error) => &error.to_string(),
|
||||
|
||||
@@ -13,6 +13,7 @@ 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),
|
||||
@@ -21,7 +22,7 @@ pub enum RemoteAccessError {
|
||||
UnparseableResponse(String),
|
||||
ManifestDownloadFailed(StatusCode, String),
|
||||
OutOfSync,
|
||||
Cache(cacache::Error),
|
||||
Cache(std::io::Error),
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
@@ -29,7 +30,10 @@ 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!(
|
||||
@@ -38,24 +42,44 @@ impl Display for RemoteAccessError {
|
||||
error,
|
||||
error
|
||||
.source()
|
||||
.map(|e| e.to_string())
|
||||
.map(std::string::ToString::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!(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!(
|
||||
RemoteAccessError::InvalidResponse(error) => write!(
|
||||
f,
|
||||
"failed to download game manifest: {status} {response}"
|
||||
"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"
|
||||
),
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
@@ -66,6 +90,11 @@ 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)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
@@ -6,111 +5,105 @@ use crate::{
|
||||
DB,
|
||||
database::db::DatabaseImpls,
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
remote::{auth::generate_authorization_header, requests::make_request},
|
||||
remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC},
|
||||
};
|
||||
|
||||
use super::collection::{Collection, Collections};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||
let client = Client::new();
|
||||
let response = make_request(&client, &["/api/v1/client/collection"], &[], async |r| {
|
||||
r.header("Authorization", generate_authorization_header().await)
|
||||
})
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send()?;
|
||||
|
||||
Ok(response.json().await?)
|
||||
Ok(response.json()?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = Client::new();
|
||||
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/collection/", &collection_id],
|
||||
&[],
|
||||
async |r| r.header("Authorization", generate_authorization_header().await),
|
||||
)
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
|r| r.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
Ok(response.json().await?)
|
||||
Ok(response.json()?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = Client::new();
|
||||
let base_url = DB.fetch_base_url().await;
|
||||
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
|
||||
|
||||
let response = client
|
||||
.post(base_url)
|
||||
.header("Authorization", generate_authorization_header().await)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.json(&json!({"name": name}))
|
||||
.send()
|
||||
.await?;
|
||||
.send()?;
|
||||
|
||||
Ok(response.json().await?)
|
||||
Ok(response.json()?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_game_to_collection(
|
||||
pub fn add_game_to_collection(
|
||||
collection_id: String,
|
||||
game_id: String,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let client = Client::new();
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let url = Url::parse(&format!(
|
||||
"{}api/v1/client/collection/{}/entry/",
|
||||
DB.fetch_base_url().await,
|
||||
DB.fetch_base_url(),
|
||||
collection_id
|
||||
))?;
|
||||
|
||||
client
|
||||
.post(url)
|
||||
.header("Authorization", generate_authorization_header().await)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.json(&json!({"id": game_id}))
|
||||
.send()
|
||||
.await?;
|
||||
.send()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_collection(collection_id: String) -> Result<(), RemoteAccessError> {
|
||||
let client = Client::new();
|
||||
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let base_url = Url::parse(&format!(
|
||||
"{}api/v1/client/collection/{}",
|
||||
DB.fetch_base_url().await,
|
||||
DB.fetch_base_url(),
|
||||
collection_id
|
||||
))?;
|
||||
|
||||
client
|
||||
let response = client
|
||||
.delete(base_url)
|
||||
.header("Authorization", generate_authorization_header().await)
|
||||
.send().await?;
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
Ok(response.json()?)
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn delete_game_in_collection(
|
||||
pub fn delete_game_in_collection(
|
||||
collection_id: String,
|
||||
game_id: String,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let client = Client::new();
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let base_url = Url::parse(&format!(
|
||||
"{}api/v1/client/collection/{}/entry",
|
||||
DB.fetch_base_url().await,
|
||||
DB.fetch_base_url(),
|
||||
collection_id
|
||||
))?;
|
||||
|
||||
client
|
||||
.delete(base_url)
|
||||
.header("Authorization", generate_authorization_header().await)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.json(&json!({"id": game_id}))
|
||||
.send().await?;
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
use serde::Deserialize;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
database::{db::borrow_db_mut_checked, models::data::GameVersion}, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{
|
||||
AppState,
|
||||
database::{
|
||||
db::borrow_db_checked,
|
||||
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, DropFunctionState
|
||||
},
|
||||
offline,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -17,8 +25,8 @@ use super::{
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_library(
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
pub fn fetch_library(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
offline!(
|
||||
state,
|
||||
@@ -26,13 +34,12 @@ pub async fn fetch_library(
|
||||
fetch_library_logic_offline,
|
||||
state
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_game(
|
||||
pub fn fetch_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
offline!(
|
||||
state,
|
||||
@@ -41,74 +48,29 @@ pub async fn fetch_game(
|
||||
game_id,
|
||||
state
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||
GameStatusManager::fetch_state(&id).await
|
||||
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||
let db_handle = borrow_db_checked();
|
||||
GameStatusManager::fetch_state(&id, &db_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
||||
let meta = match get_current_meta(&game_id).await {
|
||||
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
||||
let meta = match get_current_meta(&game_id) {
|
||||
Some(data) => data,
|
||||
None => return Err(LibraryError::MetaNotFound(game_id)),
|
||||
};
|
||||
uninstall_game_logic(meta, &app_handle).await;
|
||||
uninstall_game_logic(meta, &app_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_game_verion_options(
|
||||
pub fn fetch_game_verion_options(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
fetch_game_verion_options_logic(game_id, state).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrontendGameOptions {
|
||||
launch_string: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_game_configuration(
|
||||
game_id: String,
|
||||
options: FrontendGameOptions,
|
||||
) -> Result<(), LibraryError> {
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
let installed_version = handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&game_id)
|
||||
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
||||
|
||||
let id = installed_version.id.clone();
|
||||
let version = installed_version.version.clone().unwrap();
|
||||
|
||||
let mut existing_configuration = handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&id)
|
||||
.unwrap()
|
||||
.get(&version)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// Add more options in here
|
||||
existing_configuration.launch_command_template = options.launch_string;
|
||||
|
||||
// Add no more options past here
|
||||
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.insert(version.to_string(), existing_configuration);
|
||||
|
||||
Ok(())
|
||||
fetch_game_verion_options_logic(game_id, state)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -8,38 +8,39 @@ use crate::{
|
||||
download_manager::{
|
||||
download_manager_frontend::DownloadManagerSignal, downloadable::Downloadable,
|
||||
},
|
||||
error::download_manager_error::DownloadManagerError, DropFunctionState,
|
||||
error::download_manager_error::DownloadManagerError,
|
||||
AppState,
|
||||
};
|
||||
|
||||
use super::download_agent::GameDownloadAgent;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_game(
|
||||
pub fn download_game(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
install_dir: usize,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||
let sender = state.lock().await.download_manager.get_sender();
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
|
||||
game_id,
|
||||
game_version,
|
||||
install_dir,
|
||||
sender,
|
||||
).await) as Box<dyn Downloadable + Send + Sync>);
|
||||
)) as Box<dyn Downloadable + Send + Sync>);
|
||||
Ok(state
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn resume_download(
|
||||
pub fn resume_download(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||
let s = borrow_db_checked().await
|
||||
let s = borrow_db_checked()
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game_id)
|
||||
@@ -55,7 +56,7 @@ pub async fn resume_download(
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
};
|
||||
let sender = state.lock().await.download_manager.get_sender();
|
||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||
let parent_dir: PathBuf = install_dir.into();
|
||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||
game_id,
|
||||
@@ -65,7 +66,7 @@ pub async fn resume_download(
|
||||
)) as Box<dyn Downloadable + Send + Sync>);
|
||||
Ok(state
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.download_manager
|
||||
.queue_download(game_download_agent)?)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::DB;
|
||||
use crate::auth::generate_authorization_header;
|
||||
use crate::database::db::{DatabaseImpls, borrow_db_checked, borrow_db_mut_checked};
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||
use crate::database::models::data::{
|
||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata,
|
||||
};
|
||||
@@ -13,10 +12,15 @@ 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::game_validate_logic;
|
||||
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
|
||||
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::remote::requests::make_request;
|
||||
use log::{debug, error, info, warn};
|
||||
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::path::{Path, PathBuf};
|
||||
@@ -24,7 +28,6 @@ use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use rustix::fs::{FallocateFlags, fallocate};
|
||||
@@ -32,12 +35,6 @@ use rustix::fs::{FallocateFlags, fallocate};
|
||||
use super::download_logic::download_game_chunk;
|
||||
use super::drop_data::DropData;
|
||||
|
||||
// This is cursed but necessary
|
||||
// See the message where it is used
|
||||
unsafe fn extend_lifetime<'b, R>(r: &'b R) -> &'static R {
|
||||
unsafe { std::mem::transmute::<&'b R, &'static R>(r) }
|
||||
}
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
@@ -47,18 +44,18 @@ pub struct GameDownloadAgent {
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
pub stored_manifest: DropData,
|
||||
pub dropdata: DropData,
|
||||
status: Mutex<DownloadStatus>,
|
||||
}
|
||||
|
||||
impl GameDownloadAgent {
|
||||
pub async fn new_from_index(
|
||||
pub fn new_from_index(
|
||||
id: String,
|
||||
version: String,
|
||||
target_download_dir: usize,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
) -> Self {
|
||||
let db_lock = borrow_db_checked().await;
|
||||
let db_lock = borrow_db_checked();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
@@ -88,43 +85,46 @@ impl GameDownloadAgent {
|
||||
context_map: Mutex::new(HashMap::new()),
|
||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||
sender,
|
||||
stored_manifest,
|
||||
dropdata: stored_manifest,
|
||||
status: Mutex::new(DownloadStatus::Queued),
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub async fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists().await?;
|
||||
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
|
||||
self.ensure_contexts()?;
|
||||
|
||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Blocking
|
||||
pub async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
debug!("starting download");
|
||||
self.setup_download().await?;
|
||||
self.set_progress_object_params();
|
||||
let timer = Instant::now();
|
||||
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,
|
||||
(
|
||||
None,
|
||||
Some(ApplicationTransientStatus::Downloading {
|
||||
version_name: self.version.clone(),
|
||||
}),
|
||||
),
|
||||
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)?;
|
||||
let timer = Instant::now();
|
||||
|
||||
info!("beginning download for {}...", self.metadata().id);
|
||||
|
||||
let res = self
|
||||
.run()
|
||||
.await
|
||||
.map_err(|_| ApplicationDownloadError::DownloadError);
|
||||
.map_err(|()| ApplicationDownloadError::DownloadError);
|
||||
|
||||
debug!(
|
||||
"{} took {}ms to download",
|
||||
@@ -134,39 +134,37 @@ impl GameDownloadAgent {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||
if self.manifest.lock().unwrap().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.download_manifest().await
|
||||
self.download_manifest()
|
||||
}
|
||||
|
||||
async fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let header = generate_authorization_header().await;
|
||||
let client = reqwest::Client::new();
|
||||
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||
let header = generate_authorization_header();
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/manifest"],
|
||||
&[("id", &self.id), ("version", &self.version)],
|
||||
async |f| f.header("Authorization", header),
|
||||
|f| f.header("Authorization", header),
|
||||
)
|
||||
.await
|
||||
.map_err(ApplicationDownloadError::Communication)?
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::ManifestDownloadFailed(
|
||||
response.status(),
|
||||
response.text().await.unwrap(),
|
||||
response.text().unwrap(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download: DropManifest = response.json().await.unwrap();
|
||||
let manifest_download: DropManifest = response.json().unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download);
|
||||
@@ -176,12 +174,8 @@ impl GameDownloadAgent {
|
||||
Err(ApplicationDownloadError::Lock)
|
||||
}
|
||||
|
||||
fn set_progress_object_params(&self) {
|
||||
// Avoid re-setting it
|
||||
if self.progress.get_max() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sets it up for both download and validate
|
||||
fn setup_progress(&self) {
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
|
||||
let length = contexts.len();
|
||||
@@ -190,7 +184,7 @@ impl GameDownloadAgent {
|
||||
|
||||
self.progress.set_max(chunk_count);
|
||||
self.progress.set_size(length);
|
||||
self.progress.set_time_now();
|
||||
self.progress.reset();
|
||||
}
|
||||
|
||||
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||
@@ -198,10 +192,7 @@ impl GameDownloadAgent {
|
||||
self.generate_contexts()?;
|
||||
}
|
||||
|
||||
self.context_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.extend(self.stored_manifest.get_contexts());
|
||||
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -211,7 +202,7 @@ impl GameDownloadAgent {
|
||||
let game_id = self.id.clone();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let base_path = Path::new(&self.stored_manifest.base_path);
|
||||
let base_path = Path::new(&self.dropdata.base_path);
|
||||
create_dir_all(base_path).unwrap();
|
||||
|
||||
for (raw_path, chunk) in manifest {
|
||||
@@ -220,11 +211,12 @@ 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;
|
||||
@@ -245,12 +237,12 @@ impl GameDownloadAgent {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 {
|
||||
if running_offset > 0 && !already_exists {
|
||||
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||
}
|
||||
}
|
||||
let existing_contexts = self.stored_manifest.get_completed_contexts();
|
||||
self.stored_manifest.set_contexts(
|
||||
let existing_contexts = self.dropdata.get_completed_contexts();
|
||||
self.dropdata.set_contexts(
|
||||
&contexts
|
||||
.iter()
|
||||
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
|
||||
@@ -262,87 +254,71 @@ impl GameDownloadAgent {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<bool, ()> {
|
||||
let max_download_threads = borrow_db_checked().await.settings.max_download_threads;
|
||||
fn run(&self) -> Result<bool, ()> {
|
||||
self.setup_progress();
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"downloading game: {} with {} threads",
|
||||
self.id, max_download_threads
|
||||
);
|
||||
|
||||
let base_url = DB
|
||||
.fetch_base_url()
|
||||
.await
|
||||
.join("/api/v1/client/chunk")
|
||||
.unwrap();
|
||||
let client = reqwest::Client::new();
|
||||
let client_ref = unsafe { extend_lifetime(&client) };
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(max_download_threads)
|
||||
.thread_name("drop-download-thread")
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
let pool = ThreadPoolBuilder::new()
|
||||
.num_threads(max_download_threads)
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
warn!("failed to create download scheduler: {e}");
|
||||
()
|
||||
})?;
|
||||
.unwrap();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel(32);
|
||||
|
||||
// Scope this for safety
|
||||
{
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
debug!("{contexts:#?}");
|
||||
let completed_contexts = Arc::new(boxcar::Vec::new());
|
||||
let completed_indexes_loop_arc = completed_contexts.clone();
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
pool.scope(|scope| {
|
||||
let client = &DROP_CLIENT_SYNC.clone();
|
||||
let context_map = self.context_map.lock().unwrap();
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let client = client.clone();
|
||||
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||
|
||||
let progress = self.progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||
|
||||
// If we've done this one already, skip it
|
||||
if Some(&true) == context_map.get(&context.checksum) {
|
||||
// Note to future DecDuck, DropData gets loaded into context_map
|
||||
if let Some(v) = context_map.get(&context.checksum)
|
||||
&& *v
|
||||
{
|
||||
progress_handle.skip(context.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
let sender = self.sender.clone();
|
||||
|
||||
let local_tx = tx.clone();
|
||||
/*
|
||||
This lifetime extensions are necessary, because this loop acts like a scope
|
||||
but Rust doesn't know that.
|
||||
*/
|
||||
let context = unsafe { extend_lifetime(context) };
|
||||
let self_static = unsafe { extend_lifetime(self) };
|
||||
let mut base_url = base_url.clone();
|
||||
rt.spawn(async move {
|
||||
{
|
||||
let mut query = base_url.query_pairs_mut();
|
||||
let query_params = [
|
||||
("id", &context.game_id),
|
||||
("version", &context.version),
|
||||
("name", &context.file_name),
|
||||
("chunk", &context.index.to_string()),
|
||||
];
|
||||
for (param, val) in query_params {
|
||||
query.append_pair(param.as_ref(), val.as_ref());
|
||||
}
|
||||
let request = match make_request(
|
||||
&client,
|
||||
&["/api/v1/client/chunk"],
|
||||
&[
|
||||
("id", &context.game_id),
|
||||
("version", &context.version),
|
||||
("name", &context.file_name),
|
||||
("chunk", &context.index.to_string()),
|
||||
],
|
||||
|r| r,
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
sender
|
||||
.send(DownloadManagerSignal::Error(
|
||||
ApplicationDownloadError::Communication(e),
|
||||
))
|
||||
.unwrap();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let request = client_ref.get(base_url);
|
||||
|
||||
match download_game_chunk(
|
||||
context,
|
||||
&self_static.control_flag,
|
||||
progress_handle,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
scope.spawn(move |_| {
|
||||
match download_game_chunk(context, &self.control_flag, progress_handle, request)
|
||||
{
|
||||
Ok(true) => {
|
||||
local_tx.send(context.checksum.clone()).await.unwrap();
|
||||
completed_indexes.push(context.checksum.clone());
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
@@ -352,35 +328,32 @@ impl GameDownloadAgent {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut newly_completed = Vec::new();
|
||||
while let Some(completed_checksum) = rx.recv().await {
|
||||
newly_completed.push(completed_checksum);
|
||||
}
|
||||
let newly_completed = completed_contexts.clone();
|
||||
|
||||
// 'return' from the download
|
||||
let mut context_map_lock = self.context_map.lock().unwrap();
|
||||
for item in newly_completed.iter() {
|
||||
context_map_lock.insert(item.clone(), true);
|
||||
}
|
||||
let completed_lock_len = context_map_lock.values().filter(|x| **x).count();
|
||||
let completed_lock_len = {
|
||||
let mut context_map_lock = self.context_map.lock().unwrap();
|
||||
for (_, item) in newly_completed.iter() {
|
||||
context_map_lock.insert(item.clone(), true);
|
||||
}
|
||||
|
||||
let contexts = self.contexts.lock().unwrap();
|
||||
context_map_lock.values().filter(|x| **x).count()
|
||||
};
|
||||
let context_map_lock = self.context_map.lock().unwrap();
|
||||
let contexts = contexts
|
||||
.iter()
|
||||
.map(|x| {
|
||||
(
|
||||
x.checksum.clone(),
|
||||
context_map_lock.get(&x.checksum).cloned().unwrap_or(false),
|
||||
context_map_lock.get(&x.checksum).copied().unwrap_or(false),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(String, bool)>>();
|
||||
|
||||
drop(context_map_lock);
|
||||
|
||||
self.stored_manifest.set_contexts(&contexts);
|
||||
self.stored_manifest.write();
|
||||
self.dropdata.set_contexts(&contexts);
|
||||
self.dropdata.write();
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !contexts.iter().all(|x| x.1) {
|
||||
@@ -395,10 +368,114 @@ 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();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Downloadable for GameDownloadAgent {
|
||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||
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()
|
||||
}
|
||||
|
||||
fn control_flag(&self) -> DownloadThreadControl {
|
||||
self.control_flag.clone()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> DownloadableMetadata {
|
||||
DownloadableMetadata {
|
||||
id: self.id.clone(),
|
||||
@@ -407,25 +484,11 @@ impl Downloadable for GameDownloadAgent {
|
||||
}
|
||||
}
|
||||
|
||||
async fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||
debug!("starting download from downloadable trait");
|
||||
*self.status.lock().unwrap() = DownloadStatus::Downloading;
|
||||
self.download(app_handle).await
|
||||
}
|
||||
|
||||
async fn progress(&self) -> Arc<ProgressObject> {
|
||||
self.progress.clone()
|
||||
}
|
||||
|
||||
async fn control_flag(&self) -> DownloadThreadControl {
|
||||
self.control_flag.clone()
|
||||
}
|
||||
|
||||
async fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||
}
|
||||
|
||||
async fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Error;
|
||||
app_handle
|
||||
.emit("download_error", error.to_string())
|
||||
@@ -433,49 +496,35 @@ impl Downloadable for GameDownloadAgent {
|
||||
|
||||
error!("error while managing download: {error}");
|
||||
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
.remove(&self.metadata());
|
||||
}
|
||||
|
||||
async fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||
on_game_complete(
|
||||
&self.metadata(),
|
||||
self.stored_manifest.base_path.to_string_lossy().to_string(),
|
||||
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||
app_handle,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async 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,
|
||||
)
|
||||
.await
|
||||
.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();
|
||||
*/
|
||||
}
|
||||
|
||||
async fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
|
||||
|
||||
async fn status(&self) -> DownloadStatus {
|
||||
fn status(&self) -> DownloadStatus {
|
||||
self.status.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
async fn validate(&self) -> Result<bool, ApplicationDownloadError> {
|
||||
*self.status.lock().unwrap() = DownloadStatus::Validating;
|
||||
let contexts = self.contexts.lock().unwrap().clone();
|
||||
game_validate_logic(
|
||||
&self.stored_manifest,
|
||||
contexts,
|
||||
self.progress.clone(),
|
||||
self.sender.clone(),
|
||||
&self.control_flag,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,71 +6,70 @@ use crate::error::application_download_error::ApplicationDownloadError;
|
||||
use crate::error::drop_server_error::DropServerError;
|
||||
use crate::error::remote_access_error::RemoteAccessError;
|
||||
use crate::games::downloads::manifest::DropDownloadContext;
|
||||
use crate::remote::auth::{generate_authorization_header, generate_authorization_header_part};
|
||||
use futures::TryStreamExt;
|
||||
use log::{debug, info, warn};
|
||||
use crate::remote::auth::generate_authorization_header;
|
||||
use log::{debug, warn};
|
||||
use md5::{Context, Digest};
|
||||
use reqwest::RequestBuilder;
|
||||
use tokio::fs::{File, OpenOptions};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio_util::io::StreamReader;
|
||||
use reqwest::blocking::{RequestBuilder, Response};
|
||||
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
use std::io::Write;
|
||||
use std::fs::{set_permissions, Permissions};
|
||||
use std::io::Read;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{
|
||||
io::{self, SeekFrom},
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
pub struct DropWriter<W: AsyncWrite> {
|
||||
pub struct DropWriter<W: Write> {
|
||||
hasher: Context,
|
||||
destination: W,
|
||||
}
|
||||
impl DropWriter<File> {
|
||||
async fn new(path: PathBuf) -> Self {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
let destination = OpenOptions::new().write(true).create(true).truncate(false).open(&path).unwrap();
|
||||
Self {
|
||||
destination: OpenOptions::new().write(true).open(path).await.unwrap(),
|
||||
destination,
|
||||
hasher: Context::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn finish(mut self) -> io::Result<Digest> {
|
||||
self.flush().await.unwrap();
|
||||
fn finish(mut self) -> io::Result<Digest> {
|
||||
self.flush().unwrap();
|
||||
Ok(self.hasher.compute())
|
||||
}
|
||||
|
||||
async fn write(&mut self, mut buf: &[u8]) -> io::Result<()> {
|
||||
}
|
||||
// Write automatically pushes to file and hasher
|
||||
impl Write for DropWriter<File> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.hasher
|
||||
.write_all(buf)
|
||||
.map_err(|e| io::Error::other(format!("Unable to write to hasher: {e}")))?;
|
||||
self.destination.write_all_buf(&mut buf).await
|
||||
self.destination.write(buf)
|
||||
}
|
||||
|
||||
async fn flush(&mut self) -> io::Result<()> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.hasher.flush()?;
|
||||
self.destination.flush().await
|
||||
self.destination.flush()
|
||||
}
|
||||
|
||||
async fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.destination.seek(pos).await
|
||||
}
|
||||
// Seek moves around destination output
|
||||
impl Seek for DropWriter<File> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.destination.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DropDownloadPipeline<'a, R: AsyncRead, W: AsyncWrite> {
|
||||
pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
||||
pub source: R,
|
||||
pub destination: DropWriter<W>,
|
||||
pub control_flag: &'a DownloadThreadControl,
|
||||
pub progress: ProgressHandle,
|
||||
pub size: usize,
|
||||
}
|
||||
impl<'a, R> DropDownloadPipeline<'a, R, File>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||
fn new(
|
||||
source: R,
|
||||
source: Response,
|
||||
destination: DropWriter<File>,
|
||||
control_flag: &'a DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
@@ -85,19 +84,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn copy(&mut self) -> Result<bool, io::Error> {
|
||||
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||
let copy_buf_size = 512;
|
||||
let mut copy_buf = vec![0; copy_buf_size];
|
||||
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||
|
||||
let mut current_size = 0;
|
||||
loop {
|
||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||
self.destination.flush().await?;
|
||||
buf_writer.flush()?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut bytes_read = self.source.read(&mut copy_buf).await?;
|
||||
info!("read {}", bytes_read);
|
||||
let mut bytes_read = self.source.read(&mut copy_buf)?;
|
||||
current_size += bytes_read;
|
||||
|
||||
if current_size > self.size {
|
||||
@@ -107,7 +106,7 @@ where
|
||||
current_size = self.size;
|
||||
}
|
||||
|
||||
self.destination.write(©_buf[0..bytes_read]).await?;
|
||||
buf_writer.write_all(©_buf[0..bytes_read])?;
|
||||
self.progress.add(bytes_read);
|
||||
|
||||
if current_size >= self.size {
|
||||
@@ -118,18 +117,18 @@ where
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.destination.flush().await?;
|
||||
buf_writer.flush()?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn finish(self) -> Result<Digest, io::Error> {
|
||||
let checksum = self.destination.finish().await?;
|
||||
fn finish(self) -> Result<Digest, io::Error> {
|
||||
let checksum = self.destination.finish()?;
|
||||
Ok(checksum)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_game_chunk(
|
||||
pub fn download_game_chunk(
|
||||
ctx: &DropDownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
progress: ProgressHandle,
|
||||
@@ -140,34 +139,29 @@ pub async fn download_game_chunk(
|
||||
progress.set(0);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let header_generator = generate_authorization_header_part().await;
|
||||
|
||||
let response = request
|
||||
.header("Authorization", header_generator())
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||
|
||||
if response.status() != 200 {
|
||||
debug!("chunk request got status code: {}", response.status());
|
||||
let raw_res = response.text().await.unwrap();
|
||||
let raw_res = response.text().unwrap();
|
||||
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(err),
|
||||
));
|
||||
};
|
||||
}
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::UnparseableResponse(raw_res),
|
||||
));
|
||||
}
|
||||
|
||||
let mut destination = DropWriter::new(ctx.path.clone()).await;
|
||||
let mut destination = DropWriter::new(ctx.path.clone());
|
||||
|
||||
if ctx.offset != 0 {
|
||||
destination
|
||||
.seek(SeekFrom::Start(ctx.offset))
|
||||
.await
|
||||
.expect("Failed to seek to file offset");
|
||||
}
|
||||
|
||||
@@ -175,7 +169,7 @@ pub async fn download_game_chunk(
|
||||
if content_length.is_none() {
|
||||
warn!("recieved 0 length content from server");
|
||||
return Err(ApplicationDownloadError::Communication(
|
||||
RemoteAccessError::InvalidResponse(response.json().await.unwrap()),
|
||||
RemoteAccessError::InvalidResponse(response.json().unwrap()),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -185,21 +179,15 @@ pub async fn download_game_chunk(
|
||||
return Err(ApplicationDownloadError::DownloadError);
|
||||
}
|
||||
|
||||
let response_stream = StreamReader::new(
|
||||
response
|
||||
.bytes_stream()
|
||||
.map_err(|e| std::io::Error::other(e)),
|
||||
);
|
||||
let mut pipeline =
|
||||
DropDownloadPipeline::new(response_stream, destination, control_flag, progress, length);
|
||||
DropDownloadPipeline::new(response, destination, control_flag, progress, length);
|
||||
|
||||
let completed = pipeline
|
||||
.copy()
|
||||
.await
|
||||
.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)]
|
||||
@@ -210,7 +198,6 @@ pub async fn download_game_chunk(
|
||||
|
||||
let checksum = pipeline
|
||||
.finish()
|
||||
.await
|
||||
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||
|
||||
let res = hex::encode(checksum.0);
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf}
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
use log::error;
|
||||
use native_model::{Decode, Encode};
|
||||
|
||||
pub type DropData = v1::DropData;
|
||||
|
||||
static DROP_DATA_PATH: &str = ".dropdata";
|
||||
pub static DROP_DATA_PATH: &str = ".dropdata";
|
||||
|
||||
pub mod v1 {
|
||||
use std::{path::PathBuf, sync::Mutex};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||
|
||||
use native_model::native_model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -22,7 +20,7 @@ pub mod v1 {
|
||||
pub struct DropData {
|
||||
pub game_id: String,
|
||||
pub game_version: String,
|
||||
pub contexts: Mutex<Vec<(String, bool)>>,
|
||||
pub contexts: Mutex<HashMap<String, bool>>,
|
||||
pub base_path: PathBuf,
|
||||
}
|
||||
|
||||
@@ -32,7 +30,7 @@ pub mod v1 {
|
||||
base_path,
|
||||
game_id,
|
||||
game_version,
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
contexts: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,30 +38,18 @@ pub mod v1 {
|
||||
|
||||
impl DropData {
|
||||
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
|
||||
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
debug!("Generating new dropdata for game {game_id}");
|
||||
return DropData::new(game_id, game_version, base_path);
|
||||
}
|
||||
};
|
||||
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 s = Vec::new();
|
||||
match file.read_to_end(&mut s) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
return DropData::new(game_id, game_version, base_path);
|
||||
}
|
||||
};
|
||||
file.read_to_end(&mut s)?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap())
|
||||
}
|
||||
pub fn write(&self) {
|
||||
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
|
||||
@@ -80,26 +66,25 @@ 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.to_owned();
|
||||
*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);
|
||||
}
|
||||
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) -> Vec<(String, bool)> {
|
||||
info!(
|
||||
"Any contexts which are complete? {}",
|
||||
self.contexts.lock().unwrap().iter().any(|x| x.1)
|
||||
);
|
||||
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
||||
self.contexts.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod commands;
|
||||
pub mod download_agent;
|
||||
mod download_logic;
|
||||
mod drop_data;
|
||||
pub mod drop_data;
|
||||
mod manifest;
|
||||
pub mod validate;
|
||||
|
||||
@@ -1,88 +1,22 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
sync::{Arc, mpsc::Sender},
|
||||
};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use log::debug;
|
||||
use md5::Context;
|
||||
|
||||
use crate::{
|
||||
database::db::borrow_db_checked,
|
||||
download_manager::{
|
||||
download_manager_frontend::DownloadManagerSignal,
|
||||
download_manager::
|
||||
util::{
|
||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||
progress_object::{ProgressHandle, ProgressObject},
|
||||
},
|
||||
},
|
||||
progress_object::ProgressHandle,
|
||||
}
|
||||
,
|
||||
error::application_download_error::ApplicationDownloadError,
|
||||
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
|
||||
games::downloads::manifest::DropDownloadContext,
|
||||
};
|
||||
|
||||
pub async fn game_validate_logic(
|
||||
dropdata: &DropData,
|
||||
contexts: Vec<DropDownloadContext>,
|
||||
progress: Arc<ProgressObject>,
|
||||
sender: Sender<DownloadManagerSignal>,
|
||||
control_flag: &DownloadThreadControl,
|
||||
) -> Result<bool, ApplicationDownloadError> {
|
||||
progress.reset(contexts.len());
|
||||
let max_download_threads = borrow_db_checked().await.settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"validating game: {} with {} threads",
|
||||
dropdata.game_id, max_download_threads
|
||||
);
|
||||
|
||||
debug!("{contexts:#?}");
|
||||
let invalid_chunks = Arc::new(boxcar::Vec::new());
|
||||
unsafe {
|
||||
async_scoped::TokioScope::scope_and_collect(|scope| {
|
||||
for (index, context) in contexts.iter().enumerate() {
|
||||
let current_progress = progress.get(index);
|
||||
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
|
||||
let invalid_chunks_scoped = invalid_chunks.clone();
|
||||
let sender = sender.clone();
|
||||
|
||||
scope.spawn(async move {
|
||||
match validate_game_chunk(context, control_flag, progress_handle) {
|
||||
Ok(true) => {
|
||||
debug!(
|
||||
"Finished context #{} with checksum {}",
|
||||
index, context.checksum
|
||||
);
|
||||
}
|
||||
Ok(false) => {
|
||||
debug!(
|
||||
"Didn't finish context #{} with checksum {}",
|
||||
index, &context.checksum
|
||||
);
|
||||
invalid_chunks_scoped.push(context.checksum.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
sender.send(DownloadManagerSignal::Error(e)).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).await
|
||||
};
|
||||
|
||||
// If there are any contexts left which are false
|
||||
if !invalid_chunks.is_empty() {
|
||||
info!(
|
||||
"validation of game id {} failed for chunks {:?}",
|
||||
dropdata.game_id.clone(),
|
||||
invalid_chunks
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn validate_game_chunk(
|
||||
ctx: &DropDownloadContext,
|
||||
control_flag: &DownloadThreadControl,
|
||||
@@ -98,7 +32,9 @@ pub fn validate_game_chunk(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut source = File::open(&ctx.path).unwrap();
|
||||
let Ok(mut source) = File::open(&ctx.path) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if ctx.offset != 0 {
|
||||
source
|
||||
@@ -112,14 +48,10 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
use std::fs::remove_dir_all;
|
||||
use std::sync::Mutex;
|
||||
use std::thread::spawn;
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use tauri::Emitter;
|
||||
use tokio::spawn;
|
||||
|
||||
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,
|
||||
};
|
||||
use crate::download_manager::download_manager_frontend::DownloadStatus;
|
||||
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::DropFunctionState;
|
||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||
use bitcode::{Decode, Encode};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -71,32 +76,30 @@ pub struct StatsUpdateEvent {
|
||||
pub time: usize,
|
||||
}
|
||||
|
||||
pub async fn fetch_library_logic(
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
pub fn fetch_library_logic(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let header = generate_authorization_header().await;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = make_request(&client, &["/api/v1/client/user/library"], &[], async |f| {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
})?
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().await.unwrap();
|
||||
let err = response.json().unwrap();
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let mut games: Vec<Game> = response.json().await?;
|
||||
let mut games: Vec<Game> = response.json()?;
|
||||
|
||||
let mut handle = state.lock().await;
|
||||
let mut handle = state.lock().unwrap();
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
for game in games.iter() {
|
||||
for game in &games {
|
||||
handle.games.insert(game.id.clone(), game.clone());
|
||||
if !db_handle.applications.game_statuses.contains_key(&game.id) {
|
||||
db_handle
|
||||
@@ -113,22 +116,22 @@ pub async 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::<String, Game>(meta.id.clone(), &db_handle).await?;
|
||||
let game = get_cached_object_db::<Game>(&meta.id.clone(), &db_handle)?;
|
||||
games.push(game);
|
||||
}
|
||||
|
||||
drop(handle);
|
||||
drop(db_handle);
|
||||
cache_object("library", &games).await?;
|
||||
cache_object("library", &games)?;
|
||||
|
||||
Ok(games)
|
||||
}
|
||||
pub async fn fetch_library_logic_offline(
|
||||
_state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
pub fn fetch_library_logic_offline(
|
||||
_state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||
let mut games: Vec<Game> = get_cached_object("library").await?;
|
||||
let mut games: Vec<Game> = get_cached_object("library")?;
|
||||
|
||||
let db_handle = borrow_db_checked().await;
|
||||
let db_handle = borrow_db_checked();
|
||||
|
||||
games.retain(|game| {
|
||||
db_handle
|
||||
@@ -139,33 +142,28 @@ pub async fn fetch_library_logic_offline(
|
||||
|
||||
Ok(games)
|
||||
}
|
||||
pub async fn fetch_game_logic(
|
||||
pub fn fetch_game_logic(
|
||||
id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let mut state_handle = state.lock().await;
|
||||
let mut state_handle = state.lock().unwrap();
|
||||
|
||||
let handle = borrow_db_checked().await;
|
||||
let db_lock = borrow_db_checked();
|
||||
|
||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => Some(
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.unwrap()
|
||||
.get(metadata.version.as_ref().unwrap())
|
||||
.unwrap()
|
||||
.clone(),
|
||||
),
|
||||
Some(metadata) => db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
||||
.cloned(),
|
||||
};
|
||||
drop(handle);
|
||||
|
||||
let game = state_handle.games.get(&id);
|
||||
if let Some(game) = game {
|
||||
let status = GameStatusManager::fetch_state(&id).await;
|
||||
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
@@ -173,40 +171,41 @@ pub async fn fetch_game_logic(
|
||||
version,
|
||||
};
|
||||
|
||||
cache_object(id, game).await?;
|
||||
cache_object_db(&id, game, &db_lock)?;
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
let client = reqwest::Client::new();
|
||||
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], async |r| {
|
||||
r.header("Authorization", generate_authorization_header().await)
|
||||
})
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
drop(db_lock);
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send()?;
|
||||
|
||||
if response.status() == 404 {
|
||||
return Err(RemoteAccessError::GameNotFound(id));
|
||||
}
|
||||
if response.status() != 200 {
|
||||
let err = response.json().await.unwrap();
|
||||
let err = response.json().unwrap();
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let game: Game = response.json().await?;
|
||||
let game: Game = response.json()?;
|
||||
state_handle.games.insert(id.clone(), game.clone());
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(id.clone())
|
||||
.or_insert(GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id).await;
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
let data = FetchGameStruct {
|
||||
game: game.clone(),
|
||||
@@ -214,21 +213,21 @@ pub async fn fetch_game_logic(
|
||||
version,
|
||||
};
|
||||
|
||||
cache_object(id, &game).await?;
|
||||
cache_object(&id, &game)?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn fetch_game_logic_offline(
|
||||
pub fn fetch_game_logic_offline(
|
||||
id: String,
|
||||
_state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
_state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||
let handle = borrow_db_checked().await;
|
||||
let metadata_option = handle.applications.installed_game_version.get(&id);
|
||||
let db_handle = borrow_db_checked();
|
||||
let metadata_option = db_handle.applications.installed_game_version.get(&id);
|
||||
let version = match metadata_option {
|
||||
None => None,
|
||||
Some(metadata) => Some(
|
||||
handle
|
||||
db_handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&metadata.id)
|
||||
@@ -238,10 +237,11 @@ pub async fn fetch_game_logic_offline(
|
||||
.clone(),
|
||||
),
|
||||
};
|
||||
drop(handle);
|
||||
|
||||
let status = GameStatusManager::fetch_state(&id).await;
|
||||
let game = get_cached_object::<String, Game>(id).await?;
|
||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||
let game = get_cached_object::<Game>(&id)?;
|
||||
|
||||
drop(db_handle);
|
||||
|
||||
Ok(FetchGameStruct {
|
||||
game,
|
||||
@@ -250,35 +250,37 @@ pub async fn fetch_game_logic_offline(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_game_verion_options_logic(
|
||||
pub fn fetch_game_verion_options_logic(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||
let client = reqwest::Client::new();
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/versions"],
|
||||
&[("id", &game_id)],
|
||||
async |r| r.header("Authorization", generate_authorization_header().await),
|
||||
)
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
|r| r.header("Authorization", generate_authorization_header()),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let err = response.json().await.unwrap();
|
||||
let err = response.json().unwrap();
|
||||
warn!("{err:?}");
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
let data: Vec<GameVersion> = response.json().await?;
|
||||
let data: Vec<GameVersion> = response.json()?;
|
||||
|
||||
let state_lock = state.lock().await;
|
||||
let process_manager_lock = state_lock.process_manager.lock().await;
|
||||
let state_lock = state.lock().unwrap();
|
||||
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).unwrap())
|
||||
.filter(|v| {
|
||||
process_manager_lock
|
||||
.valid_platform(&v.platform, &state_lock)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
@@ -286,9 +288,52 @@ pub async fn fetch_game_verion_options_logic(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
/**
|
||||
* 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().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
@@ -299,7 +344,7 @@ pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHa
|
||||
app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
(None, Some(ApplicationTransientStatus::Uninstalling {})),
|
||||
GameStatusManager::fetch_state(&meta.id, &db_handle),
|
||||
);
|
||||
|
||||
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
|
||||
@@ -333,52 +378,50 @@ pub async fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHa
|
||||
drop(db_handle);
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
spawn(async move {
|
||||
match remove_dir_all(install_dir) {
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
spawn(move || {
|
||||
if let Err(e) = remove_dir_all(install_dir) {
|
||||
error!("{e}");
|
||||
} else {
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.remove(&meta.id);
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
push_game_update(
|
||||
&app_handle,
|
||||
&meta.id,
|
||||
None,
|
||||
(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.");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
borrow_db_checked()
|
||||
.await
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(game_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn on_game_incomplete(
|
||||
pub fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
@@ -388,7 +431,9 @@ pub async fn on_game_incomplete(
|
||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/version"],
|
||||
@@ -396,79 +441,13 @@ pub async fn on_game_incomplete(
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
async |f| f.header("Authorization", generate_authorization_header().await),
|
||||
)
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
|f| f.header("Authorization", header),
|
||||
)?
|
||||
.send()?;
|
||||
|
||||
let game_version: GameVersion = response.json().await?;
|
||||
let game_version: GameVersion = response.json()?;
|
||||
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.entry(meta.id.clone())
|
||||
.or_default()
|
||||
.insert(meta.version.clone().unwrap(), game_version.clone());
|
||||
handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
let status = GameDownloadStatus::PartiallyInstalled {
|
||||
version_name: meta.version.clone().unwrap(),
|
||||
install_dir,
|
||||
};
|
||||
|
||||
handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(handle);
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
GameUpdateEvent {
|
||||
game_id: meta.id.clone(),
|
||||
status: (Some(status), None),
|
||||
version: Some(game_version),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn on_game_complete(
|
||||
meta: &DownloadableMetadata,
|
||||
install_dir: String,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
// Fetch game version information from remote
|
||||
if meta.version.is_none() {
|
||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||
}
|
||||
|
||||
let header = generate_authorization_header().await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/game/version"],
|
||||
&[
|
||||
("id", &meta.id),
|
||||
("version", meta.version.as_ref().unwrap()),
|
||||
],
|
||||
async |f| f.header("Authorization", header),
|
||||
)
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let game_version: GameVersion = response.json().await?;
|
||||
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
@@ -494,7 +473,7 @@ pub async fn on_game_complete(
|
||||
}
|
||||
};
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
@@ -531,3 +510,48 @@ pub fn push_game_update(
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrontendGameOptions {
|
||||
launch_string: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_game_configuration(
|
||||
game_id: String,
|
||||
options: FrontendGameOptions,
|
||||
) -> Result<(), LibraryError> {
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let installed_version = handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(&game_id)
|
||||
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
||||
|
||||
let id = installed_version.id.clone();
|
||||
let version = installed_version.version.clone().unwrap();
|
||||
|
||||
let mut existing_configuration = handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get(&id)
|
||||
.unwrap()
|
||||
.get(&version)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// Add more options in here
|
||||
existing_configuration.launch_command_template = options.launch_string;
|
||||
|
||||
// Add no more options past here
|
||||
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.insert(version.to_string(), existing_configuration);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::database::{
|
||||
db::borrow_db_checked,
|
||||
models::data::{ApplicationTransientStatus, GameDownloadStatus},
|
||||
};
|
||||
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus};
|
||||
|
||||
pub type GameStatusWithTransient = (
|
||||
Option<GameDownloadStatus>,
|
||||
@@ -10,14 +7,12 @@ pub type GameStatusWithTransient = (
|
||||
pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
pub async fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = borrow_db_checked().await;
|
||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
||||
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(),
|
||||
None => None,
|
||||
};
|
||||
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
|
||||
drop(db_lock);
|
||||
let offline_state = database.applications.game_statuses.get(game_id).cloned();
|
||||
|
||||
if online_state.is_some() {
|
||||
return (None, online_state);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#![feature(fn_traits)]
|
||||
#![feature(duration_constructors)]
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
#![deny(clippy::all)]
|
||||
|
||||
mod database;
|
||||
@@ -12,9 +11,10 @@ mod error;
|
||||
mod process;
|
||||
mod remote;
|
||||
|
||||
use crate::database::db::OnceCellDatabase;
|
||||
use crate::games::commands::update_game_configuration;
|
||||
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;
|
||||
@@ -41,7 +41,7 @@ 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;
|
||||
use games::library::{Game, update_game_configuration};
|
||||
use log::{LevelFilter, debug, info, warn};
|
||||
use log4rs::Config;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
@@ -55,24 +55,27 @@ 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, fetch_object_offline};
|
||||
use remote::fetch_object::fetch_object;
|
||||
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
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;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
use std::{env, panic};
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
|
||||
pub enum AppStatus {
|
||||
@@ -95,6 +98,27 @@ 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> {
|
||||
@@ -106,9 +130,11 @@ pub struct AppState<'a> {
|
||||
download_manager: Arc<DownloadManager>,
|
||||
#[serde(skip_serializing)]
|
||||
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||
#[serde(skip_serializing)]
|
||||
compat_info: Option<CompatInfo>,
|
||||
}
|
||||
|
||||
async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
let logfile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||
@@ -142,9 +168,13 @@ async 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().await;
|
||||
let is_set_up = DB.database_is_set_up();
|
||||
|
||||
scan_install_dirs();
|
||||
|
||||
if !is_set_up {
|
||||
return AppState {
|
||||
status: AppStatus::NotConfigured,
|
||||
@@ -152,19 +182,21 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_info,
|
||||
};
|
||||
}
|
||||
|
||||
debug!("database is set up");
|
||||
|
||||
// TODO: Account for possible failure
|
||||
let (app_status, user) = auth::setup().await;
|
||||
let (app_status, user) = auth::setup();
|
||||
|
||||
let db_handle = borrow_db_checked().await;
|
||||
let db_handle = borrow_db_checked();
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.applications.game_statuses.clone();
|
||||
drop(db_handle);
|
||||
for (game_id, status) in statuses.into_iter() {
|
||||
|
||||
for (game_id, status) in statuses {
|
||||
match status {
|
||||
GameDownloadStatus::Remote {} => {}
|
||||
GameDownloadStatus::PartiallyInstalled { .. } => {}
|
||||
@@ -191,7 +223,7 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
|
||||
info!("detected games missing: {missing_games:?}");
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
for game_id in missing_games {
|
||||
db_handle
|
||||
.applications
|
||||
@@ -205,7 +237,7 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
debug!("finished setup!");
|
||||
|
||||
// Sync autostart state
|
||||
if let Err(e) = sync_autostart_on_startup(&handle).await {
|
||||
if let Err(e) = sync_autostart_on_startup(&handle) {
|
||||
warn!("failed to sync autostart state: {e}");
|
||||
}
|
||||
|
||||
@@ -215,11 +247,11 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
games,
|
||||
download_manager,
|
||||
process_manager,
|
||||
compat_info,
|
||||
}
|
||||
}
|
||||
|
||||
pub static DB: OnceCellDatabase = OnceCellDatabase::new();
|
||||
pub type DropFunctionState<'a> = Mutex<AppState<'a>>;
|
||||
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||
|
||||
pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
|
||||
let crash_file = DATA_ROOT_DIR.join(format!(
|
||||
@@ -269,6 +301,7 @@ pub fn run() {
|
||||
fetch_settings,
|
||||
// Auth
|
||||
auth_initiate,
|
||||
auth_initiate_code,
|
||||
retry_connect,
|
||||
manual_recieve_handshake,
|
||||
sign_out,
|
||||
@@ -314,142 +347,109 @@ pub fn run() {
|
||||
Some(vec!["--minimize"]),
|
||||
))
|
||||
.setup(|app| {
|
||||
let app = app.handle().clone();
|
||||
let handle = app.handle().clone();
|
||||
let state = setup(handle);
|
||||
debug!("initialized drop client");
|
||||
app.manage(Mutex::new(state));
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
DB.init(async { DatabaseInterface::set_up_database().await })
|
||||
.await;
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
let _ = app.deep_link().register_all();
|
||||
debug!("registered all pre-defined deep links");
|
||||
}
|
||||
|
||||
let state = setup(app.clone()).await;
|
||||
info!("initialized drop client");
|
||||
if !app.manage(Mutex::new(state)) {
|
||||
panic!("failed to setup drop state before Tauri does")
|
||||
let handle = app.handle().clone();
|
||||
|
||||
let _main_window = tauri::WebviewWindowBuilder::new(
|
||||
&handle,
|
||||
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Drop Desktop App")
|
||||
.min_inner_size(1000.0, 500.0)
|
||||
.inner_size(1536.0, 864.0)
|
||||
.decorations(false)
|
||||
.shadow(false)
|
||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
debug!("handling drop:// url");
|
||||
let binding = event.urls();
|
||||
let url = binding.first().unwrap();
|
||||
if url.host_str().unwrap() == "handshake" {
|
||||
recieve_handshake(handle.clone(), url.path().to_string());
|
||||
}
|
||||
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
let _ = app.deep_link().register_all();
|
||||
debug!("registered all pre-defined deep links");
|
||||
}
|
||||
|
||||
let deep_link_handle = app.clone();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
let deep_link_handle = deep_link_handle.clone();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
debug!("handling drop:// url");
|
||||
let binding = event.urls();
|
||||
let url = binding.first().unwrap();
|
||||
if url.host_str().unwrap() == "handshake" {
|
||||
recieve_handshake(deep_link_handle, url.path().to_string()).await
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let menu = Menu::with_items(
|
||||
&app,
|
||||
&[
|
||||
&MenuItem::with_id(&app, "open", "Open", true, None::<&str>).unwrap(),
|
||||
&PredefinedMenuItem::separator(&app).unwrap(),
|
||||
/*
|
||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
*/
|
||||
&MenuItem::with_id(&app, "quit", "Quit", true, None::<&str>).unwrap(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tray_app_handle = app.clone();
|
||||
run_on_tray(move || {
|
||||
TrayIconBuilder::new()
|
||||
.icon(tray_app_handle.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| {
|
||||
let tray_app_handle = app.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
match event.id.as_ref() {
|
||||
"open" => {
|
||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||
}
|
||||
"quit" => {
|
||||
cleanup_and_exit(
|
||||
&tray_app_handle,
|
||||
&tray_app_handle.state(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("menu event not handled: {:?}", event.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.build(&tray_app_handle)
|
||||
.expect("error while setting up tray menu");
|
||||
});
|
||||
|
||||
{
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
original.canonicalize().unwrap().to_string_lossy()
|
||||
);
|
||||
app.dialog()
|
||||
.message(
|
||||
"Database corrupted. A copy has been saved at: ".to_string()
|
||||
+ original.to_str().unwrap(),
|
||||
)
|
||||
.title("Database corrupted")
|
||||
.show(|_| {});
|
||||
}
|
||||
};
|
||||
|
||||
let _main_window = tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Drop Desktop App")
|
||||
.min_inner_size(1000.0, 500.0)
|
||||
.inner_size(1536.0, 864.0)
|
||||
.decorations(false)
|
||||
.shadow(false)
|
||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
||||
.build()
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let menu = Menu::with_items(
|
||||
app,
|
||||
&[
|
||||
&MenuItem::with_id(app, "open", "Open", true, None::<&str>)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
/*
|
||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
*/
|
||||
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
run_on_tray(|| {
|
||||
TrayIconBuilder::new()
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"open" => {
|
||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
||||
}
|
||||
"quit" => {
|
||||
cleanup_and_exit(app, &app.state());
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("menu event not handled: {:?}", event.id);
|
||||
}
|
||||
})
|
||||
.build(app)
|
||||
.expect("error while setting up tray menu");
|
||||
});
|
||||
|
||||
{
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
original.canonicalize().unwrap().to_string_lossy()
|
||||
);
|
||||
app.dialog()
|
||||
.message(
|
||||
"Database corrupted. A copy has been saved at: ".to_string()
|
||||
+ original.to_str().unwrap(),
|
||||
)
|
||||
.title("Database corrupted")
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.register_asynchronous_uri_scheme_protocol("object", move |ctx, request, responder| {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let state = ctx.app_handle().state::<DropFunctionState<'_>>();
|
||||
offline!(
|
||||
state,
|
||||
fetch_object,
|
||||
fetch_object_offline,
|
||||
request,
|
||||
responder
|
||||
)
|
||||
.await;
|
||||
.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("server", move |ctx, request, responder| {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let state = ctx.app_handle().state::<DropFunctionState<'_>>();
|
||||
offline!(
|
||||
state,
|
||||
handle_server_proto,
|
||||
handle_server_proto_offline,
|
||||
request,
|
||||
responder
|
||||
)
|
||||
.await;
|
||||
});
|
||||
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
|
||||
offline!(
|
||||
state,
|
||||
handle_server_proto,
|
||||
handle_server_proto_offline,
|
||||
request,
|
||||
responder
|
||||
);
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
|
||||
@@ -2,11 +2,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
let global_runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(32)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
tauri::async_runtime::set(global_runtime.handle().clone());
|
||||
drop_app_lib::run()
|
||||
drop_app_lib::run();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::{error::process_error::ProcessError, DropFunctionState};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::{error::process_error::ProcessError, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn launch_game(
|
||||
pub fn launch_game(
|
||||
id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let state_lock = state.lock().await;
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().await;
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
|
||||
//let meta = DownloadableMetadata {
|
||||
// id,
|
||||
@@ -14,10 +16,10 @@ pub async fn launch_game(
|
||||
// download_type: DownloadType::Game,
|
||||
//};
|
||||
|
||||
match process_manager_lock.launch_process(id).await {
|
||||
Ok(_) => {}
|
||||
match process_manager_lock.launch_process(id, &state_lock) {
|
||||
Ok(()) => {}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
drop(process_manager_lock);
|
||||
drop(state_lock);
|
||||
@@ -26,23 +28,23 @@ pub async fn launch_game(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn kill_game(
|
||||
pub fn kill_game(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let state_lock = state.lock().await;
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().await;
|
||||
let state_lock = state.lock().unwrap();
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||
process_manager_lock
|
||||
.kill_game(game_id)
|
||||
.map_err(ProcessError::IOError)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_process_logs(
|
||||
pub fn open_process_logs(
|
||||
game_id: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let state_lock = state.lock().await;
|
||||
let mut process_manager_lock = state_lock.process_manager.lock().await;
|
||||
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)
|
||||
}
|
||||
|
||||
33
src-tauri/src/process/format.rs
Normal file
33
src-tauri/src/process/format.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
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<'_>))
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,5 @@ pub mod commands;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod compat;
|
||||
pub mod process_manager;
|
||||
pub mod process_handlers;
|
||||
pub mod format;
|
||||
109
src-tauri/src/process/process_handlers.rs
Normal file
109
src-tauri/src/process/process_handlers.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ use std::{
|
||||
path::PathBuf,
|
||||
process::{Command, ExitStatus},
|
||||
str::FromStr,
|
||||
sync::{Arc},
|
||||
sync::{Arc, Mutex},
|
||||
thread::spawn,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
@@ -16,16 +17,22 @@ use serde::{Deserialize, Serialize};
|
||||
use shared_child::SharedChild;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::spawn;
|
||||
|
||||
use crate::{
|
||||
AppState, DB,
|
||||
database::{
|
||||
db::{borrow_db_mut_checked, DATA_ROOT_DIR},
|
||||
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
|
||||
models::data::{
|
||||
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||
GameVersion,
|
||||
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
|
||||
GameDownloadStatus, GameVersion,
|
||||
},
|
||||
}, error::process_error::ProcessError, games::{library::push_game_update, state::GameStatusManager}, DropFunctionState, DB
|
||||
},
|
||||
error::process_error::ProcessError,
|
||||
games::{library::push_game_update, state::GameStatusManager},
|
||||
process::{
|
||||
format::DropFormatArgs,
|
||||
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct RunningProcess {
|
||||
@@ -39,7 +46,10 @@ pub struct ProcessManager<'a> {
|
||||
log_output_dir: PathBuf,
|
||||
processes: HashMap<String, RunningProcess>,
|
||||
app_handle: AppHandle,
|
||||
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
|
||||
game_launchers: Vec<(
|
||||
(Platform, Platform),
|
||||
&'a (dyn ProcessHandler + Sync + Send + 'static),
|
||||
)>,
|
||||
}
|
||||
|
||||
impl ProcessManager<'_> {
|
||||
@@ -59,7 +69,7 @@ impl ProcessManager<'_> {
|
||||
app_handle,
|
||||
processes: HashMap::new(),
|
||||
log_output_dir,
|
||||
game_launchers: HashMap::from([
|
||||
game_launchers: vec![
|
||||
// Current platform to target platform
|
||||
(
|
||||
(Platform::Windows, Platform::Windows),
|
||||
@@ -73,11 +83,15 @@ 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),
|
||||
),
|
||||
]),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +110,12 @@ 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.log_output_dir.join(game_id);
|
||||
let dir = self.get_log_dir(game_id);
|
||||
self.app_handle
|
||||
.opener()
|
||||
.open_path(dir.to_str().unwrap(), None::<&str>)
|
||||
@@ -105,7 +123,7 @@ impl ProcessManager<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
|
||||
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:?}"
|
||||
@@ -117,7 +135,7 @@ impl ProcessManager<'_> {
|
||||
|
||||
let process = self.processes.remove(&game_id).unwrap();
|
||||
|
||||
let mut db_handle = borrow_db_mut_checked().await;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let meta = db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
@@ -142,7 +160,6 @@ impl ProcessManager<'_> {
|
||||
},
|
||||
);
|
||||
}
|
||||
drop(db_handle);
|
||||
|
||||
let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO);
|
||||
// If we started and ended really quickly, something might've gone wrong
|
||||
@@ -155,32 +172,56 @@ impl ProcessManager<'_> {
|
||||
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
||||
}
|
||||
|
||||
let status = GameStatusManager::fetch_state(&game_id).await;
|
||||
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
|
||||
drop(db_handle);
|
||||
|
||||
push_game_update(&self.app_handle, &game_id, None, status);
|
||||
}
|
||||
|
||||
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
|
||||
let current = &self.current_platform;
|
||||
Ok(self.game_launchers.contains_key(&(*current, *platform)))
|
||||
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 async fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
|
||||
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> {
|
||||
if self.processes.contains_key(&game_id) {
|
||||
return Err(ProcessError::AlreadyRunning);
|
||||
}
|
||||
|
||||
let version = match DB
|
||||
.borrow_data()
|
||||
.await
|
||||
.unwrap()
|
||||
.applications
|
||||
.game_statuses
|
||||
.get(&game_id)
|
||||
.cloned()
|
||||
{
|
||||
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
|
||||
Some(GameDownloadStatus::SetupRequired { .. }) => {
|
||||
return Err(ProcessError::SetupRequired);
|
||||
}
|
||||
Some(GameDownloadStatus::SetupRequired { version_name, .. }) => version_name,
|
||||
_ => return Err(ProcessError::NotInstalled),
|
||||
};
|
||||
let meta = DownloadableMetadata {
|
||||
@@ -189,11 +230,7 @@ impl ProcessManager<'_> {
|
||||
download_type: DownloadType::Game,
|
||||
};
|
||||
|
||||
let mut db_lock = borrow_db_mut_checked().await;
|
||||
debug!(
|
||||
"Launching process {:?} with games {:?}",
|
||||
&game_id, db_lock.applications.game_versions
|
||||
);
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
|
||||
let game_status = db_lock
|
||||
.applications
|
||||
@@ -210,13 +247,15 @@ impl ProcessManager<'_> {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
GameDownloadStatus::PartiallyInstalled {
|
||||
version_name,
|
||||
install_dir,
|
||||
} => (version_name, install_dir),
|
||||
_ => return Err(ProcessError::NotDownloaded),
|
||||
_ => return Err(ProcessError::NotInstalled),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Launching process {:?} with version {:?}",
|
||||
&game_id,
|
||||
db_lock.applications.game_versions.get(&game_id).unwrap()
|
||||
);
|
||||
|
||||
let game_version = db_lock
|
||||
.applications
|
||||
.game_versions
|
||||
@@ -226,7 +265,7 @@ impl ProcessManager<'_> {
|
||||
.ok_or(ProcessError::InvalidVersion)?;
|
||||
|
||||
// TODO: refactor this path with open_process_logs
|
||||
let game_log_folder = &self.log_output_dir.join(game_id);
|
||||
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();
|
||||
@@ -250,13 +289,9 @@ impl ProcessManager<'_> {
|
||||
)))
|
||||
.map_err(ProcessError::IOError)?;
|
||||
|
||||
let current_platform = self.current_platform;
|
||||
let target_platform = game_version.platform;
|
||||
|
||||
let game_launcher = self
|
||||
.game_launchers
|
||||
.get(&(current_platform, target_platform))
|
||||
.ok_or(ProcessError::InvalidPlatform)?;
|
||||
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
|
||||
|
||||
let (launch, args) = match game_status {
|
||||
GameDownloadStatus::Installed {
|
||||
@@ -277,16 +312,23 @@ impl ProcessManager<'_> {
|
||||
let launch = PathBuf::from_str(install_dir).unwrap().join(launch);
|
||||
let launch = launch.to_str().unwrap();
|
||||
|
||||
let launch_string = game_launcher.create_launch_process(
|
||||
let launch_string = process_handler.create_launch_process(
|
||||
&meta,
|
||||
launch.to_string(),
|
||||
args.to_vec(),
|
||||
args.clone(),
|
||||
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, &[launch_string])
|
||||
.format(&game_version.launch_command_template, format_args)
|
||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||
.to_string();
|
||||
|
||||
@@ -305,9 +347,12 @@ impl ProcessManager<'_> {
|
||||
#[cfg(unix)]
|
||||
command.args(vec!["-c", &launch_string]);
|
||||
|
||||
debug!("final launch string:\n\n{launch_string}\n");
|
||||
|
||||
command
|
||||
.stderr(error_file)
|
||||
.stdout(log_file)
|
||||
.env_remove("RUST_LOG")
|
||||
.current_dir(install_dir);
|
||||
|
||||
let child = command.spawn().map_err(ProcessError::IOError)?;
|
||||
@@ -331,14 +376,14 @@ impl ProcessManager<'_> {
|
||||
let wait_thread_apphandle = self.app_handle.clone();
|
||||
let wait_thread_game_id = meta.clone();
|
||||
|
||||
spawn(async move {
|
||||
spawn(move || {
|
||||
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
||||
|
||||
let app_state = wait_thread_apphandle.state::<tauri::State<'_, DropFunctionState<'_>>>();
|
||||
let app_state_handle = app_state.lock().await;
|
||||
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
||||
let app_state_handle = app_state.lock().unwrap();
|
||||
|
||||
let mut process_manager_handle = app_state_handle.process_manager.lock().await;
|
||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result).await;
|
||||
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
||||
|
||||
// As everything goes out of scope, they should get dropped
|
||||
// But just to explicit about it
|
||||
@@ -412,49 +457,6 @@ pub trait ProcessHandler: Send + 'static {
|
||||
game_version: &GameVersion,
|
||||
current_dir: &str,
|
||||
) -> String;
|
||||
}
|
||||
|
||||
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(" ")
|
||||
)
|
||||
}
|
||||
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use std::{collections::HashMap, env};
|
||||
use std::{collections::HashMap, env, sync::Mutex};
|
||||
|
||||
use chrono::Utc;
|
||||
use droplet_rs::ssl::sign_nonce;
|
||||
use gethostname::gethostname;
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
AppStatus, DropFunctionState, User,
|
||||
database::{
|
||||
db::{borrow_db_checked, borrow_db_mut_checked},
|
||||
models::data::DatabaseAuth,
|
||||
},
|
||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::utils::DROP_CLIENT_SYNC, AppState, AppStatus, User
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -32,6 +30,7 @@ struct InitiateRequestBody {
|
||||
name: String,
|
||||
platform: String,
|
||||
capabilities: HashMap<String, CapabilityConfiguration>,
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -49,38 +48,29 @@ struct HandshakeResponse {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub async fn generate_authorization_header() -> String {
|
||||
let func = generate_authorization_header_part().await;
|
||||
func()
|
||||
}
|
||||
|
||||
pub async fn generate_authorization_header_part() -> Box<dyn FnOnce() -> String> {
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = borrow_db_checked().await;
|
||||
let db = borrow_db_checked();
|
||||
db.auth.clone().unwrap()
|
||||
};
|
||||
|
||||
Box::new(move || {
|
||||
let nonce = Utc::now().timestamp_millis().to_string();
|
||||
let nonce = Utc::now().timestamp_millis().to_string();
|
||||
|
||||
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
||||
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
||||
|
||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||
})
|
||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||
}
|
||||
|
||||
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let header = generate_authorization_header().await;
|
||||
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = make_request(&client, &["/api/v1/client/user"], &[], async |f| {
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
|
||||
f.header("Authorization", header)
|
||||
})
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
})?
|
||||
.send()?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json().await?;
|
||||
let err: DropServerError = response.json()?;
|
||||
warn!("{err:?}");
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
@@ -90,11 +80,11 @@ pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
response.json::<User>().await.map_err(|e| e.into())
|
||||
response.json::<User>().map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||
let path_chunks: Vec<&str> = path.split("/").collect();
|
||||
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||
let path_chunks: Vec<&str> = path.split('/').collect();
|
||||
if path_chunks.len() != 3 {
|
||||
app.emit("auth/failed", ()).unwrap();
|
||||
return Err(RemoteAccessError::HandshakeFailed(
|
||||
@@ -103,28 +93,28 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
||||
}
|
||||
|
||||
let base_url = {
|
||||
let handle = borrow_db_checked().await;
|
||||
let handle = borrow_db_checked();
|
||||
Url::parse(handle.base_url.as_str())?
|
||||
};
|
||||
|
||||
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 = reqwest::Client::new();
|
||||
let response = client.post(endpoint).json(&body).send().await?;
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client.post(endpoint).json(&body).send()?;
|
||||
debug!("handshake responsded with {}", response.status().as_u16());
|
||||
if !response.status().is_success() {
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json()?));
|
||||
}
|
||||
let response_struct: HandshakeResponse = response.json().await?;
|
||||
let response_struct: HandshakeResponse = response.json()?;
|
||||
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = Some(DatabaseAuth {
|
||||
private: response_struct.private,
|
||||
cert: response_struct.certificate,
|
||||
@@ -134,49 +124,50 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
||||
}
|
||||
|
||||
let web_token = {
|
||||
let header = generate_authorization_header().await;
|
||||
let header = generate_authorization_header();
|
||||
let token = client
|
||||
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
token.text().await.unwrap()
|
||||
token.text().unwrap()
|
||||
};
|
||||
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let mut_auth = handle.auth.as_mut().unwrap();
|
||||
mut_auth.web_token = Some(web_token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
||||
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
// Tell the app we're processing
|
||||
app.emit("auth/processing", ()).unwrap();
|
||||
|
||||
let handshake_result = recieve_handshake_logic(&app, path).await;
|
||||
let handshake_result = recieve_handshake_logic(&app, path);
|
||||
if let Err(e) = handshake_result {
|
||||
warn!("error with authentication: {e}");
|
||||
app.emit("auth/failed", e.to_string()).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
let app_state = app.state::<DropFunctionState<'_>>();
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut state_lock = app_state.lock().unwrap();
|
||||
|
||||
let (app_status, user) = setup().await;
|
||||
let (app_status, user) = setup();
|
||||
|
||||
let mut state_lock = app_state.lock().await;
|
||||
state_lock.status = app_status;
|
||||
state_lock.user = user;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
app.emit("auth/finished", ()).unwrap();
|
||||
}
|
||||
|
||||
pub async fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = borrow_db_checked().await;
|
||||
let db_lock = borrow_db_checked();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
};
|
||||
|
||||
@@ -190,42 +181,39 @@ pub async fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
||||
]),
|
||||
mode,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send().await?;
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let data: DropServerError = response.json().await?;
|
||||
let data: DropServerError = response.json()?;
|
||||
error!("could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
||||
let redir_url = response.text().await?;
|
||||
let complete_redir_url = base_url.join(&redir_url)?;
|
||||
let response = response.text()?;
|
||||
|
||||
info!("opening web browser to continue authentication: {}", complete_redir_url);
|
||||
webbrowser::open(complete_redir_url.as_ref()).unwrap();
|
||||
|
||||
Ok(())
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn setup() -> (AppStatus, Option<User>) {
|
||||
let data = borrow_db_checked().await;
|
||||
pub fn setup() -> (AppStatus, Option<User>) {
|
||||
let data = borrow_db_checked();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
if auth.is_some() {
|
||||
let user_result = match fetch_user().await {
|
||||
let user_result = match fetch_user() {
|
||||
Ok(data) => data,
|
||||
Err(RemoteAccessError::FetchError(_)) => {
|
||||
let user = get_cached_object::<_, User>("user").await.unwrap();
|
||||
let user = get_cached_object::<User>("user").unwrap();
|
||||
return (AppStatus::Offline, Some(user));
|
||||
}
|
||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||
};
|
||||
cache_object("user", &user_result).await.unwrap();
|
||||
cache_object("user", &user_result).unwrap();
|
||||
return (AppStatus::SignedIn, Some(user_result));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,83 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
time::{Duration, SystemTime},
|
||||
fs::File,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
db::borrow_db_checked,
|
||||
models::data::Database,
|
||||
},
|
||||
database::{db::borrow_db_checked, models::data::Database},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
};
|
||||
use bitcode::{Decode, DecodeOwned, Encode};
|
||||
use cacache::Integrity;
|
||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||
use log::info;
|
||||
use log::debug;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! offline {
|
||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||
|
||||
(async || if $crate::borrow_db_checked().await.settings.force_offline || $var.lock().await.status == $crate::AppStatus::Offline {
|
||||
$func2( $( $arg ), *).await
|
||||
if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
||||
$func2( $( $arg ), *)
|
||||
} else {
|
||||
$func1( $( $arg ), *).await
|
||||
})()
|
||||
$func1( $( $arg ), *)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cache_object<K: AsRef<str>, D: Encode>(
|
||||
key: K,
|
||||
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,
|
||||
data: &D,
|
||||
) -> Result<Integrity, RemoteAccessError> {
|
||||
database: &Database,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let bytes = bitcode::encode(data);
|
||||
cacache::write_sync(&borrow_db_checked().await.cache_dir, key, bytes)
|
||||
.map_err(RemoteAccessError::Cache)
|
||||
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
|
||||
}
|
||||
pub async fn get_cached_object<K: AsRef<str> + Display, D: Encode + DecodeOwned>(
|
||||
key: K,
|
||||
) -> Result<D, RemoteAccessError> {
|
||||
get_cached_object_db::<K, D>(key, &&(borrow_db_checked().await)).await
|
||||
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
|
||||
get_cached_object_db::<D>(key, &borrow_db_checked())
|
||||
}
|
||||
pub async fn get_cached_object_db<'a, K: AsRef<str> + Display, D: DecodeOwned>(
|
||||
key: K,
|
||||
pub fn get_cached_object_db<D: DecodeOwned>(
|
||||
key: &str,
|
||||
db: &Database,
|
||||
) -> Result<D, RemoteAccessError> {
|
||||
let start = SystemTime::now();
|
||||
let bytes = cacache::read(&db.cache_dir, &key)
|
||||
.await
|
||||
.map_err(RemoteAccessError::Cache)?;
|
||||
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(|_| {
|
||||
RemoteAccessError::Cache(cacache::Error::EntryNotFound(
|
||||
db.cache_dir.clone(),
|
||||
key.to_string(),
|
||||
))
|
||||
})?;
|
||||
let parse = start.elapsed().unwrap().abs_diff(read);
|
||||
info!(
|
||||
"read object: r: {}, p: {}, b: {}",
|
||||
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(),
|
||||
parse.as_millis(),
|
||||
read.abs_diff(decode).as_millis(),
|
||||
bytes.len()
|
||||
);
|
||||
Ok(data)
|
||||
@@ -68,17 +86,13 @@ pub async fn get_cached_object_db<'a, K: AsRef<str> + Display, D: DecodeOwned>(
|
||||
pub struct ObjectCache {
|
||||
content_type: String,
|
||||
body: Vec<u8>,
|
||||
expiry: u128,
|
||||
expiry: u64,
|
||||
}
|
||||
|
||||
impl ObjectCache {
|
||||
pub fn has_expired(&self) -> bool {
|
||||
let duration = Duration::from_millis(self.expiry.try_into().unwrap());
|
||||
SystemTime::UNIX_EPOCH
|
||||
.checked_add(duration)
|
||||
.unwrap()
|
||||
.elapsed()
|
||||
.is_err()
|
||||
let current = get_sys_time_in_secs();
|
||||
self.expiry < current
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +107,7 @@ impl From<Response<Vec<u8>>> for ObjectCache {
|
||||
.unwrap()
|
||||
.to_owned(),
|
||||
body: value.body().clone(),
|
||||
expiry: SystemTime::now()
|
||||
.checked_add(Duration::from_days(1))
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
expiry: get_sys_time_in_secs() + 60 * 60 * 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use log::debug;
|
||||
use reqwest::Client;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use futures_lite::StreamExt;
|
||||
use log::{debug, warn};
|
||||
use reqwest_websocket::{Message, RequestBuilderExt};
|
||||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
AppStatus, DropFunctionState,
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
remote::{auth::generate_authorization_header, requests::make_request},
|
||||
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
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -17,17 +18,17 @@ use super::{
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn use_remote(
|
||||
pub fn use_remote(
|
||||
url: String,
|
||||
state: tauri::State<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
use_remote_logic(url, state).await
|
||||
use_remote_logic(url, state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let handle = borrow_db_checked().await;
|
||||
let handle = borrow_db_checked();
|
||||
|
||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||
};
|
||||
@@ -38,39 +39,37 @@ pub async fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
||||
let _drop_url = gen_drop_url(path.clone()).await?;
|
||||
let req = make_request(&Client::new(), &[&path], &[], async |r| {
|
||||
r.header("Authorization", generate_authorization_header().await)
|
||||
})
|
||||
.await?
|
||||
.send()
|
||||
.await;
|
||||
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| {
|
||||
r.header("Authorization", generate_authorization_header())
|
||||
})?
|
||||
.send();
|
||||
|
||||
match req {
|
||||
Ok(data) => {
|
||||
let data = data.bytes().await?.to_vec();
|
||||
cache_object(&path, &data).await?;
|
||||
let data = data.bytes()?.to_vec();
|
||||
cache_object(&path, &data)?;
|
||||
Ok(data)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("{e}");
|
||||
get_cached_object::<&str, Vec<u8>>(&path).await
|
||||
get_cached_object::<Vec<u8>>(&path)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn sign_out(app: AppHandle) {
|
||||
pub fn sign_out(app: AppHandle) {
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked().await;
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = None;
|
||||
}
|
||||
|
||||
// Update app state
|
||||
{
|
||||
let app_state = app.state::<DropFunctionState<'_>>();
|
||||
let mut app_state_handle = app_state.lock().await;
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut app_state_handle = app_state.lock().unwrap();
|
||||
app_state_handle.status = AppStatus::SignedOut;
|
||||
app_state_handle.user = None;
|
||||
}
|
||||
@@ -80,23 +79,92 @@ pub async fn sign_out(app: AppHandle) {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn retry_connect(state: tauri::State<'_, DropFunctionState<'_>>) -> Result<(), ()> {
|
||||
let (app_status, user) = setup().await;
|
||||
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
|
||||
let (app_status, user) = setup();
|
||||
|
||||
let mut guard = state.lock().await;
|
||||
let mut guard = state.lock().unwrap();
|
||||
guard.status = app_status;
|
||||
guard.user = user;
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn auth_initiate() -> Result<(), RemoteAccessError> {
|
||||
auth_initiate_logic().await
|
||||
#[derive(Deserialize)]
|
||||
struct CodeWebsocketResponse {
|
||||
#[serde(rename = "type")]
|
||||
response_type: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||
recieve_handshake(app, format!("handshake/{token}")).await;
|
||||
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)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||
recieve_handshake(app, format!("handshake/{token}"));
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ 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},
|
||||
requests::make_request,
|
||||
};
|
||||
|
||||
pub async 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::<&str, ObjectCache>(object_id).await;
|
||||
let cache_result = get_cached_object::<ObjectCache>(object_id);
|
||||
if let Ok(cache_result) = &cache_result
|
||||
&& !cache_result.has_expired()
|
||||
{
|
||||
@@ -20,23 +21,16 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
|
||||
return;
|
||||
}
|
||||
|
||||
let header = generate_authorization_header().await;
|
||||
let client = reqwest::Client::new();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&["/api/v1/client/object/", object_id],
|
||||
&[],
|
||||
async |f| f.header("Authorization", header),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.send()
|
||||
.await;
|
||||
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;
|
||||
|
||||
if response.is_err() {
|
||||
match cache_result {
|
||||
Ok(cache_result) => responder.respond(cache_result.into()),
|
||||
Err(e) => {
|
||||
warn!("{e}")
|
||||
warn!("{e}");
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -50,19 +44,8 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
|
||||
let data = Vec::from(response.bytes().await.unwrap());
|
||||
let resp = resp_builder.body(data).unwrap();
|
||||
if cache_result.is_err() || cache_result.unwrap().has_expired() {
|
||||
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into())
|
||||
.await
|
||||
.unwrap();
|
||||
cache_object::<ObjectCache>(object_id, &resp.clone().into()).unwrap();
|
||||
}
|
||||
|
||||
responder.respond(resp);
|
||||
}
|
||||
pub async 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).await;
|
||||
|
||||
match data {
|
||||
Ok(data) => responder.respond(data.into()),
|
||||
Err(e) => warn!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use reqwest::blocking::{Client, RequestBuilder};
|
||||
|
||||
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
|
||||
|
||||
pub async fn make_request<T: AsRef<str>, F: AsyncFnOnce(RequestBuilder) -> RequestBuilder>(
|
||||
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
|
||||
client: &Client,
|
||||
path_components: &[T],
|
||||
query: &[(T, T)],
|
||||
f: F,
|
||||
) -> Result<RequestBuilder, RemoteAccessError> {
|
||||
let mut base_url = DB.fetch_base_url().await;
|
||||
let mut base_url = DB.fetch_base_url();
|
||||
for endpoint in path_components {
|
||||
base_url = base_url.join(endpoint.as_ref())?;
|
||||
}
|
||||
@@ -19,5 +19,5 @@ pub async fn make_request<T: AsRef<str>, F: AsyncFnOnce(RequestBuilder) -> Reque
|
||||
}
|
||||
}
|
||||
let response = client.get(base_url);
|
||||
Ok(f(response).await)
|
||||
Ok(f(response))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
||||
use reqwest::Client;
|
||||
use tauri::UriSchemeResponder;
|
||||
|
||||
use crate::database::db::borrow_db_checked;
|
||||
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC};
|
||||
|
||||
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
let four_oh_four = Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Vec::new())
|
||||
@@ -14,8 +13,8 @@ pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder:
|
||||
responder.respond(four_oh_four);
|
||||
}
|
||||
|
||||
pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
let db_handle = borrow_db_checked().await;
|
||||
pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
let db_handle = borrow_db_checked();
|
||||
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
||||
Some(e) => e,
|
||||
None => return,
|
||||
@@ -38,17 +37,16 @@ pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriScheme
|
||||
return;
|
||||
}
|
||||
|
||||
let client = Client::new();
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client
|
||||
.request(request.method().clone(), new_uri.to_string())
|
||||
.header("Authorization", format!("Bearer {web_token}"))
|
||||
.headers(request.headers().clone())
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response_status = response.status();
|
||||
let response_body = response.bytes().await.unwrap();
|
||||
let response_body = response.bytes().unwrap();
|
||||
|
||||
let http_response = Response::builder()
|
||||
.status(response_status)
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Read,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use log::{debug, info, warn};
|
||||
use reqwest::Certificate;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppStatus, DropFunctionState
|
||||
AppState, AppStatus,
|
||||
database::db::{DATA_ROOT_DIR, borrow_db_mut_checked},
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -12,30 +21,84 @@ struct DropHealthcheck {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
pub async fn use_remote_logic(
|
||||
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<'_, DropFunctionState<'_>>,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
info!("connecting to url {url}");
|
||||
debug!("connecting to url {url}");
|
||||
let base_url = Url::parse(&url)?;
|
||||
|
||||
// Test Drop url
|
||||
let test_endpoint = base_url.join("/api/v1")?;
|
||||
let response = reqwest::get(test_endpoint.to_string()).await?;
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client.get(test_endpoint.to_string()).send()?;
|
||||
|
||||
let result: DropHealthcheck = response.json().await?;
|
||||
let result: DropHealthcheck = response.json()?;
|
||||
|
||||
if result.app_name != "Drop" {
|
||||
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
||||
return Err(RemoteAccessError::InvalidEndpoint);
|
||||
}
|
||||
|
||||
{
|
||||
let mut app_state = state.lock().await;
|
||||
app_state.status = AppStatus::SignedOut;
|
||||
}
|
||||
let mut app_state = state.lock().unwrap();
|
||||
app_state.status = AppStatus::SignedOut;
|
||||
drop(app_state);
|
||||
|
||||
let mut db_state = borrow_db_mut_checked().await;
|
||||
let mut db_state = borrow_db_mut_checked();
|
||||
db_state.base_url = base_url.to_string();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "Drop Desktop Client",
|
||||
"version": "0.3.0-rc-8",
|
||||
"version": "0.3.1-appimage",
|
||||
"identifier": "dev.drop.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev --port 1432",
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"csp": null,
|
||||
"csp": "",
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": {}
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
|
||||
"targets": ["nsis", "deb", "rpm", "dmg"],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "both"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"exclude": ["src-tauri/**/*"]
|
||||
}
|
||||
|
||||
3
types.ts
3
types.ts
@@ -54,12 +54,13 @@ 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 = {
|
||||
|
||||
Reference in New Issue
Block a user