mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-12 15:52:43 +10:00
Compare commits
1 Commits
52-feature
...
v0.3.2-dl
| Author | SHA1 | Date | |
|---|---|---|---|
| 124d51bced |
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
|||||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||||
|
|
||||||
|
|
||||||
@ -69,9 +69,9 @@ jobs:
|
|||||||
security set-keychain-settings -t 3600 -u build.keychain
|
security set-keychain-settings -t 3600 -u build.keychain
|
||||||
|
|
||||||
curl https://droposs.org/drop.crt --output drop.pem
|
curl https://droposs.org/drop.crt --output drop.pem
|
||||||
sudo security authorizationdb write com.apple.trust-settings.user allow
|
sudo security authorizationdb write com.apple.trust-settings.admin allow
|
||||||
security add-trusted-cert -r trustRoot -k build.keychain -p codeSign -u -1 drop.pem
|
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.user
|
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 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 set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||||
|
|||||||
@ -1,59 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div>
|
||||||
<div class="mb-3 inline-flex gap-x-2">
|
<div class="mb-3 inline-flex gap-x-2">
|
||||||
<div class="relative transition-transform duration-300 hover:scale-105 active:scale-95">
|
<div
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
||||||
<MagnifyingGlassIcon class="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
class="h-5 w-5 text-zinc-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" v-model="searchQuery"
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="searchQuery"
|
||||||
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
class="block w-full rounded-lg border-0 bg-zinc-800/50 py-2 pl-10 pr-3 text-zinc-100 placeholder:text-zinc-500 focus:bg-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
|
||||||
placeholder="Search library..." />
|
placeholder="Search library..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button @click="() => calculateGames(true)"
|
<button
|
||||||
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100">
|
@click="() => calculateGames(true)"
|
||||||
|
class="p-1 flex items-center justify-center transition-transform duration-300 size-10 hover:scale-110 active:scale-90 rounded-lg bg-zinc-800/50 text-zinc-100"
|
||||||
|
>
|
||||||
<ArrowPathIcon class="size-4" />
|
<ArrowPathIcon class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
<TransitionGroup name="list" tag="ul" class="flex flex-col gap-y-1.5">
|
||||||
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
|
<NuxtLink
|
||||||
|
v-for="nav in filteredNavigation"
|
||||||
|
:key="nav.id"
|
||||||
|
:class="[
|
||||||
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
'transition-all duration-300 rounded-lg flex items-center py-2 px-3 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
||||||
navIndex === currentNavigation
|
nav.index === currentNavigation
|
||||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||||
: nav.isInstalled.value
|
: nav.isInstalled.value
|
||||||
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
|
||||||
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
|
||||||
]" :href="nav.route">
|
]"
|
||||||
|
:href="nav.route"
|
||||||
|
>
|
||||||
<div class="flex items-center w-full gap-x-3">
|
<div class="flex items-center w-full gap-x-3">
|
||||||
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
|
<div
|
||||||
<img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
||||||
:src="icons[nav.id]" alt="" />
|
>
|
||||||
|
<img
|
||||||
|
class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
||||||
|
:src="icons[nav.id]"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
|
<p
|
||||||
|
class="truncate text-xs font-display leading-5 flex-1 font-semibold"
|
||||||
|
>
|
||||||
{{ nav.label }}
|
{{ nav.label }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
|
<p
|
||||||
|
class="text-xs font-medium"
|
||||||
|
:class="[gameStatusTextStyle[games[nav.id].status.value.type]]"
|
||||||
|
>
|
||||||
{{ gameStatusText[games[nav.id].status.value.type] }}
|
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
<div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
|
|
||||||
<div role="status">
|
|
||||||
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101"
|
|
||||||
fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor" />
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill" />
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -67,12 +80,12 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
// Style information
|
// Style information
|
||||||
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
||||||
[GameStatusEnum.Installed]: "text-green-500",
|
[GameStatusEnum.Installed]: "text-green-500",
|
||||||
[GameStatusEnum.Downloading]: "text-zinc-400",
|
[GameStatusEnum.Downloading]: "text-blue-500",
|
||||||
[GameStatusEnum.Validating]: "text-blue-300",
|
[GameStatusEnum.Validating]: "text-blue-300",
|
||||||
[GameStatusEnum.Running]: "text-green-500",
|
[GameStatusEnum.Running]: "text-green-500",
|
||||||
[GameStatusEnum.Remote]: "text-zinc-500",
|
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
[GameStatusEnum.Queued]: "text-blue-500",
|
||||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
[GameStatusEnum.Updating]: "text-blue-500",
|
||||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||||
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
[GameStatusEnum.SetupRequired]: "text-yellow-500",
|
||||||
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
[GameStatusEnum.PartiallyInstalled]: "text-gray-400",
|
||||||
@ -94,7 +107,6 @@ const router = useRouter();
|
|||||||
|
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const games: {
|
const games: {
|
||||||
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
[key: string]: { game: Game; status: Ref<GameStatus, GameStatus> };
|
||||||
} = {};
|
} = {};
|
||||||
@ -103,10 +115,7 @@ const icons: { [key: string]: string } = {};
|
|||||||
const rawGames: Ref<Game[], Game[]> = ref([]);
|
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||||
|
|
||||||
async function calculateGames(clearAll = false) {
|
async function calculateGames(clearAll = false) {
|
||||||
if (clearAll) {
|
if (clearAll) rawGames.value = [];
|
||||||
rawGames.value = [];
|
|
||||||
loading.value = true;
|
|
||||||
}
|
|
||||||
// If we update immediately, the navigation gets re-rendered before we
|
// If we update immediately, the navigation gets re-rendered before we
|
||||||
// add all the necessary state, and it freaks tf out
|
// add all the necessary state, and it freaks tf out
|
||||||
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||||
@ -118,22 +127,10 @@ async function calculateGames(clearAll = false) {
|
|||||||
if (icons[game.id]) continue;
|
if (icons[game.id]) continue;
|
||||||
icons[game.id] = await useObject(game.mIconObjectId);
|
icons[game.id] = await useObject(game.mIconObjectId);
|
||||||
}
|
}
|
||||||
loading.value = false;
|
|
||||||
rawGames.value = newGames;
|
rawGames.value = newGames;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to 300 ms for the library to load, otherwise
|
await calculateGames();
|
||||||
// show the loading state while we while
|
|
||||||
await new Promise<void>((r) => {
|
|
||||||
let hasResolved = false;
|
|
||||||
const resolveFunc = () => {
|
|
||||||
if (!hasResolved) r();
|
|
||||||
hasResolved = true
|
|
||||||
|
|
||||||
}
|
|
||||||
calculateGames(true).then(resolveFunc);
|
|
||||||
setTimeout(resolveFunc, 300);
|
|
||||||
})
|
|
||||||
|
|
||||||
const navigation = computed(() =>
|
const navigation = computed(() =>
|
||||||
rawGames.value.map((game) => {
|
rawGames.value.map((game) => {
|
||||||
@ -141,7 +138,8 @@ const navigation = computed(() =>
|
|||||||
|
|
||||||
const isInstalled = computed(
|
const isInstalled = computed(
|
||||||
() =>
|
() =>
|
||||||
status.value.type != GameStatusEnum.Remote
|
status.value.type == GameStatusEnum.Installed ||
|
||||||
|
status.value.type == GameStatusEnum.SetupRequired
|
||||||
);
|
);
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
@ -154,11 +152,9 @@ const navigation = computed(() =>
|
|||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
const { currentNavigation, recalculateNavigation } = useCurrentNavigationIndex(
|
||||||
const route = useRoute();
|
navigation.value
|
||||||
const currentNavigation = computed(() => {
|
);
|
||||||
return navigation.value.findIndex((e) => e.route == route.path)
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredNavigation = computed(() => {
|
const filteredNavigation = computed(() => {
|
||||||
if (!searchQuery.value)
|
if (!searchQuery.value)
|
||||||
@ -173,7 +169,9 @@ listen("update_library", async (event) => {
|
|||||||
console.log("Updating library");
|
console.log("Updating library");
|
||||||
let oldNavigation = navigation.value[currentNavigation.value];
|
let oldNavigation = navigation.value[currentNavigation.value];
|
||||||
await calculateGames();
|
await calculateGames();
|
||||||
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
|
recalculateNavigation();
|
||||||
|
if (oldNavigation !== navigation.value[currentNavigation.value]) {
|
||||||
|
console.log("Triggered");
|
||||||
router.push("/library");
|
router.push("/library");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export const useGame = async (gameId: string) => {
|
|||||||
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
|
||||||
|
|
||||||
listen(`update_game/${gameId}`, (event) => {
|
listen(`update_game/${gameId}`, (event) => {
|
||||||
console.log(event);
|
|
||||||
const payload: {
|
const payload: {
|
||||||
status: SerializedGameStatus;
|
status: SerializedGameStatus;
|
||||||
version?: GameVersion;
|
version?: GameVersion;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "view",
|
"name": "view",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.2",
|
"version": "0.3.2-dl",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt generate",
|
"build": "nuxt generate",
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
|
|
||||||
<div>
|
|
||||||
<Wordmark />
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
|
|
||||||
Under construction
|
|
||||||
</h1>
|
|
||||||
<p class="mt-6 text-lg leading-8 text-zinc-400">
|
|
||||||
Yes, we know. We're working on it <a class="text-white" target="_blank"
|
|
||||||
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -243,10 +243,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="mt-1 rounded-md bg-red-600/10 p-4">
|
||||||
v-else-if="versionOptions === null || versionOptions?.length == 0"
|
|
||||||
class="mt-1 rounded-md bg-red-600/10 p-4"
|
|
||||||
>
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||||
@ -259,27 +256,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full flex items-center justify-center p-4">
|
|
||||||
<div role="status">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="w-7 h-7 text-transparent animate-spin fill-white"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="installDirs">
|
<div v-if="installDirs">
|
||||||
<Listbox as="div" v-model="installDir">
|
<Listbox as="div" v-model="installDir">
|
||||||
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
<ListboxLabel class="block text-sm/6 font-medium text-zinc-100"
|
||||||
@ -534,13 +510,13 @@ async function installFlow() {
|
|||||||
installDirs.value = undefined;
|
installDirs.value = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
versionOptions.value = await invoke("fetch_game_version_options", {
|
versionOptions.value = await invoke("fetch_game_verion_options", {
|
||||||
gameId: game.value.id,
|
gameId: game.value.id,
|
||||||
});
|
});
|
||||||
|
console.log(versionOptions.value);
|
||||||
installDirs.value = await invoke("fetch_download_dir_stats");
|
installDirs.value = await invoke("fetch_download_dir_stats");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
installError.value = (error as string).toString();
|
installError.value = (error as string).toString();
|
||||||
versionOptions.value = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mx-auto flex flex-col items-center gap-y-4 max-w-2xl py-32 sm:py-48 lg:py-56">
|
|
||||||
<div>
|
|
||||||
<Wordmark />
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<h1 class="text-balance text-4xl font-bold font-display tracking-tight text-zinc-100 sm:text-6xl">
|
|
||||||
Under construction
|
|
||||||
</h1>
|
|
||||||
<p class="mt-6 text-lg leading-8 text-zinc-400">
|
|
||||||
Yes, we know. We're working on it <a class="text-white" target="_blank"
|
|
||||||
href="https://github.com/Drop-OSS/drop-app/issues/52">here.</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
50
src-tauri/Cargo.lock
generated
50
src-tauri/Cargo.lock
generated
@ -1284,7 +1284,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.2"
|
version = "0.3.2-dl"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-instant-full",
|
"atomic-instant-full",
|
||||||
"bitcode",
|
"bitcode",
|
||||||
@ -1312,7 +1312,7 @@ dependencies = [
|
|||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.22",
|
"reqwest 0.12.16",
|
||||||
"reqwest-middleware 0.4.2",
|
"reqwest-middleware 0.4.2",
|
||||||
"reqwest-middleware-cache",
|
"reqwest-middleware-cache",
|
||||||
"reqwest-websocket",
|
"reqwest-websocket",
|
||||||
@ -2381,7 +2381,6 @@ dependencies = [
|
|||||||
"hyper 1.6.0",
|
"hyper 1.6.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
@ -3111,7 +3110,7 @@ dependencies = [
|
|||||||
"openssl-probe",
|
"openssl-probe",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"schannel",
|
"schannel",
|
||||||
"security-framework 2.11.1",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
@ -4420,9 +4419,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.22"
|
version = "0.12.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
|
checksum = "2bf597b113be201cb2269b4c39b39a804d01b99ee95a4278f0ed04e45cff1c71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -4437,14 +4436,16 @@ dependencies = [
|
|||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"mime",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -4490,7 +4491,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"reqwest 0.12.22",
|
"reqwest 0.12.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@ -4525,7 +4526,7 @@ dependencies = [
|
|||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"reqwest 0.12.22",
|
"reqwest 0.12.16",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@ -4688,18 +4689,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-native-certs"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
|
|
||||||
dependencies = [
|
|
||||||
"openssl-probe",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"schannel",
|
|
||||||
"security-framework 3.2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@ -4797,19 +4786,6 @@ dependencies = [
|
|||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "security-framework"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.9.1",
|
|
||||||
"core-foundation 0.10.1",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
"security-framework-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework-sys"
|
name = "security-framework-sys"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@ -5530,7 +5506,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest 0.12.22",
|
"reqwest 0.12.16",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -6175,9 +6151,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.6"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.2"
|
version = "0.3.2-dl"
|
||||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||||
authors = ["Drop OSS"]
|
authors = ["Drop OSS"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@ -104,9 +104,9 @@ version = "2"
|
|||||||
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12.22"
|
version = "0.12"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"]
|
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
@ -266,7 +266,6 @@ pub mod data {
|
|||||||
pub install_dirs: Vec<PathBuf>,
|
pub install_dirs: Vec<PathBuf>,
|
||||||
// Guaranteed to exist if the game also exists in the app state map
|
// Guaranteed to exist if the game also exists in the app state map
|
||||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||||
|
|
||||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||||
|
|
||||||
|
|||||||
@ -124,16 +124,11 @@ impl DownloadManagerBuilder {
|
|||||||
self.current_download_agent = None;
|
self.current_download_agent = None;
|
||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
|
*download_thread_lock = None;
|
||||||
if let Some(unfinished_thread) = download_thread_lock.take()
|
|
||||||
&& !unfinished_thread.is_finished()
|
|
||||||
{
|
|
||||||
unfinished_thread.join().unwrap();
|
|
||||||
}
|
|
||||||
drop(download_thread_lock);
|
drop(download_thread_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_and_wait_current_download(&self) -> bool {
|
fn stop_and_wait_current_download(&self) {
|
||||||
self.set_status(DownloadManagerStatus::Paused);
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
if let Some(current_flag) = &self.active_control_flag {
|
if let Some(current_flag) = &self.active_control_flag {
|
||||||
current_flag.set(DownloadThreadControlFlag::Stop);
|
current_flag.set(DownloadThreadControlFlag::Stop);
|
||||||
@ -141,10 +136,8 @@ impl DownloadManagerBuilder {
|
|||||||
|
|
||||||
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
|
||||||
if let Some(current_download_thread) = download_thread_lock.take() {
|
if let Some(current_download_thread) = download_thread_lock.take() {
|
||||||
return current_download_thread.join().is_ok();
|
current_download_thread.join().unwrap();
|
||||||
};
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_queue(mut self) -> Result<(), ()> {
|
fn manage_queue(mut self) -> Result<(), ()> {
|
||||||
@ -220,6 +213,10 @@ impl DownloadManagerBuilder {
|
|||||||
&& self.download_queue.read().front().unwrap()
|
&& self.download_queue.read().front().unwrap()
|
||||||
== &self.current_download_agent.as_ref().unwrap().metadata()
|
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
{
|
{
|
||||||
|
debug!(
|
||||||
|
"Current download agent: {:?}",
|
||||||
|
self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,16 +254,12 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the download gets canceled
|
// If the download gets cancelled
|
||||||
// immediately return, on_cancelled gets called for us earlier
|
// immediately return, on_cancelled gets called for us earlier
|
||||||
if !download_result {
|
if !download_result {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let validate_result = match download_agent.validate(&app_handle) {
|
let validate_result = match download_agent.validate(&app_handle) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -281,10 +274,6 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if validate_result {
|
if validate_result {
|
||||||
download_agent.on_complete(&app_handle);
|
download_agent.on_complete(&app_handle);
|
||||||
sender
|
sender
|
||||||
@ -327,7 +316,6 @@ impl DownloadManagerBuilder {
|
|||||||
self.stop_and_wait_current_download();
|
self.stop_and_wait_current_download();
|
||||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||||
}
|
}
|
||||||
self.push_ui_queue_update();
|
|
||||||
self.set_status(DownloadManagerStatus::Error);
|
self.set_status(DownloadManagerStatus::Error);
|
||||||
}
|
}
|
||||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{
|
sync::{
|
||||||
Arc, Mutex,
|
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
mpsc::Sender,
|
mpsc::Sender,
|
||||||
|
Arc, Mutex,
|
||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@ -23,7 +23,7 @@ pub struct ProgressObject {
|
|||||||
//last_update: Arc<RwLock<Instant>>,
|
//last_update: Arc<RwLock<Instant>>,
|
||||||
last_update_time: Arc<AtomicInstant>,
|
last_update_time: Arc<AtomicInstant>,
|
||||||
bytes_last_update: Arc<AtomicUsize>,
|
bytes_last_update: Arc<AtomicUsize>,
|
||||||
rolling: RollingProgressWindow<1>,
|
rolling: RollingProgressWindow<10>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -128,7 +128,7 @@ pub fn calculate_update(progress: &ProgressObject) {
|
|||||||
.bytes_last_update
|
.bytes_last_update
|
||||||
.swap(current_bytes_downloaded, Ordering::Acquire);
|
.swap(current_bytes_downloaded, Ordering::Acquire);
|
||||||
|
|
||||||
let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update);
|
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
|
||||||
|
|
||||||
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
|
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Formatter},
|
fmt::{Display, Formatter},
|
||||||
io, sync::Arc,
|
io,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
@ -11,20 +11,17 @@ use super::remote_access_error::RemoteAccessError;
|
|||||||
// TODO: Rename / separate from downloads
|
// TODO: Rename / separate from downloads
|
||||||
#[derive(Debug, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum ApplicationDownloadError {
|
pub enum ApplicationDownloadError {
|
||||||
NotInitialized,
|
|
||||||
Communication(RemoteAccessError),
|
Communication(RemoteAccessError),
|
||||||
DiskFull(u64, u64),
|
DiskFull(u64, u64),
|
||||||
#[allow(dead_code)]
|
|
||||||
Checksum,
|
Checksum,
|
||||||
Lock,
|
Lock,
|
||||||
IoError(Arc<io::Error>),
|
IoError(io::ErrorKind),
|
||||||
DownloadError,
|
DownloadError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ApplicationDownloadError {
|
impl Display for ApplicationDownloadError {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ApplicationDownloadError::NotInitialized => write!(f, "Download not initalized, did something go wrong?"),
|
|
||||||
ApplicationDownloadError::DiskFull(required, available) => write!(
|
ApplicationDownloadError::DiskFull(required, available) => write!(
|
||||||
f,
|
f,
|
||||||
"Game requires {}, {} remaining left on disk.",
|
"Game requires {}, {} remaining left on disk.",
|
||||||
@ -42,7 +39,7 @@ impl Display for ApplicationDownloadError {
|
|||||||
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
|
||||||
ApplicationDownloadError::DownloadError => write!(
|
ApplicationDownloadError::DownloadError => write!(
|
||||||
f,
|
f,
|
||||||
"Download failed. See Download Manager status for specific error"
|
"download failed. See Download Manager status for specific error"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,96 +1,109 @@
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
DB,
|
||||||
|
database::db::DatabaseImpls,
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::remote_access_error::RemoteAccessError,
|
||||||
remote::{
|
remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC},
|
||||||
auth::generate_authorization_header,
|
|
||||||
requests::{generate_url, make_authenticated_get},
|
|
||||||
utils::DROP_CLIENT_ASYNC,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::collection::{Collection, Collections};
|
use super::collection::{Collection, Collections};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||||
let response =
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?;
|
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]
|
#[tauri::command]
|
||||||
pub async fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||||
let response = make_authenticated_get(generate_url(
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = make_request(
|
||||||
|
&client,
|
||||||
&["/api/v1/client/collection/", &collection_id],
|
&["/api/v1/client/collection/", &collection_id],
|
||||||
&[],
|
&[],
|
||||||
)?)
|
|r| r.header("Authorization", generate_authorization_header()),
|
||||||
.await?;
|
)?
|
||||||
|
.send()?;
|
||||||
|
|
||||||
Ok(response.json().await?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
let url = generate_url(&["/api/v1/client/collection"], &[])?;
|
let base_url = DB.fetch_base_url();
|
||||||
|
|
||||||
|
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.post(url)
|
.post(base_url)
|
||||||
.header("Authorization", generate_authorization_header())
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"name": name}))
|
.json(&json!({"name": name}))
|
||||||
.send()
|
.send()?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(response.json().await?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn add_game_to_collection(
|
pub fn add_game_to_collection(
|
||||||
collection_id: String,
|
collection_id: String,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let url = Url::parse(&format!(
|
||||||
let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?;
|
"{}api/v1/client/collection/{}/entry/",
|
||||||
|
DB.fetch_base_url(),
|
||||||
|
collection_id
|
||||||
|
))?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("Authorization", generate_authorization_header())
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"id": game_id}))
|
.json(&json!({"id": game_id}))
|
||||||
.send()
|
.send()?;
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let base_url = Url::parse(&format!(
|
||||||
let url = generate_url(&["/api/v1/client/collection", &collection_id], &[])?;
|
"{}api/v1/client/collection/{}",
|
||||||
|
DB.fetch_base_url(),
|
||||||
|
collection_id
|
||||||
|
))?;
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.delete(url)
|
.delete(base_url)
|
||||||
.header("Authorization", generate_authorization_header())
|
.header("Authorization", generate_authorization_header())
|
||||||
.send()
|
.send()?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(response.json().await?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_game_in_collection(
|
pub fn delete_game_in_collection(
|
||||||
collection_id: String,
|
collection_id: String,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let base_url = Url::parse(&format!(
|
||||||
let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?;
|
"{}api/v1/client/collection/{}/entry",
|
||||||
|
DB.fetch_base_url(),
|
||||||
|
collection_id
|
||||||
|
))?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.delete(url)
|
.delete(base_url)
|
||||||
.header("Authorization", generate_authorization_header())
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"id": game_id}))
|
.json(&json!({"id": game_id}))
|
||||||
.send().await?;
|
.send()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,28 +18,28 @@ use crate::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
library::{
|
library::{
|
||||||
FetchGameStruct, Game, fetch_game_logic, fetch_game_version_options_logic,
|
FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic,
|
||||||
fetch_library_logic,
|
fetch_library_logic,
|
||||||
},
|
},
|
||||||
state::{GameStatusManager, GameStatusWithTransient},
|
state::{GameStatusManager, GameStatusWithTransient},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_library(
|
pub fn fetch_library(
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
fetch_library_logic,
|
fetch_library_logic,
|
||||||
fetch_library_logic_offline,
|
fetch_library_logic_offline,
|
||||||
state
|
state
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_game(
|
pub fn fetch_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
@ -47,7 +47,7 @@ pub async fn fetch_game(
|
|||||||
fetch_game_logic_offline,
|
fetch_game_logic_offline,
|
||||||
game_id,
|
game_id,
|
||||||
state
|
state
|
||||||
).await
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -68,9 +68,9 @@ pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), Libr
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn fetch_game_version_options(
|
pub fn fetch_game_verion_options(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||||
fetch_game_version_options_logic(game_id, state).await
|
fetch_game_verion_options_logic(game_id, state)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,47 +3,44 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
|
||||||
database::{
|
download_manager::
|
||||||
db::borrow_db_checked,
|
downloadable::Downloadable
|
||||||
models::data::GameDownloadStatus,
|
,
|
||||||
},
|
|
||||||
download_manager::downloadable::Downloadable,
|
|
||||||
error::application_download_error::ApplicationDownloadError,
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::download_agent::GameDownloadAgent;
|
use super::download_agent::GameDownloadAgent;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_game(
|
pub fn download_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
game_version: String,
|
game_version: String,
|
||||||
install_dir: usize,
|
install_dir: usize,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ApplicationDownloadError> {
|
) -> Result<(), ApplicationDownloadError> {
|
||||||
let sender = { state.lock().unwrap().download_manager.get_sender().clone() };
|
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||||
|
let game_download_agent = GameDownloadAgent::new_from_index(
|
||||||
let game_download_agent =
|
game_id,
|
||||||
GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?;
|
game_version,
|
||||||
|
install_dir,
|
||||||
let game_download_agent =
|
sender,
|
||||||
Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
|
)?;
|
||||||
|
let game_download_agent = Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
|
||||||
state
|
state
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.download_manager
|
.download_manager
|
||||||
.queue_download(game_download_agent.clone())
|
.queue_download(game_download_agent).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn resume_download(
|
pub fn resume_download(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), ApplicationDownloadError> {
|
) -> Result<(), ApplicationDownloadError> {
|
||||||
let s = borrow_db_checked()
|
let s = borrow_db_checked()
|
||||||
.applications
|
.applications
|
||||||
@ -65,21 +62,17 @@ pub async fn resume_download(
|
|||||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||||
let parent_dir: PathBuf = install_dir.into();
|
let parent_dir: PathBuf = install_dir.into();
|
||||||
|
|
||||||
let game_download_agent = Arc::new(Box::new(
|
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||||
GameDownloadAgent::new(
|
|
||||||
game_id,
|
game_id,
|
||||||
version_name.clone(),
|
version_name.clone(),
|
||||||
parent_dir.parent().unwrap().to_path_buf(),
|
parent_dir.parent().unwrap().to_path_buf(),
|
||||||
sender,
|
sender,
|
||||||
)
|
)?) as Box<dyn Downloadable + Send + Sync>);
|
||||||
.await?,
|
|
||||||
) as Box<dyn Downloadable + Send + Sync>);
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.download_manager
|
.download_manager
|
||||||
.queue_download(game_download_agent)
|
.queue_download(game_download_agent).unwrap();
|
||||||
.unwrap();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,15 +11,13 @@ use crate::download_manager::util::download_thread_control_flag::{
|
|||||||
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::{
|
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||||
DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody,
|
|
||||||
};
|
|
||||||
use crate::games::downloads::validate::validate_game_chunk;
|
use crate::games::downloads::validate::validate_game_chunk;
|
||||||
use crate::games::library::{on_game_complete, push_game_update, set_partially_installed};
|
use crate::games::library::{on_game_complete, push_game_update, set_partially_installed};
|
||||||
use crate::games::state::GameStatusManager;
|
use crate::games::state::GameStatusManager;
|
||||||
use crate::process::utils::get_disk_available;
|
use crate::process::utils::get_disk_available;
|
||||||
use crate::remote::requests::generate_url;
|
use crate::remote::requests::make_request;
|
||||||
use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rayon::ThreadPoolBuilder;
|
use rayon::ThreadPoolBuilder;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -33,18 +31,16 @@ use tauri::{AppHandle, Emitter};
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use rustix::fs::{FallocateFlags, fallocate};
|
use rustix::fs::{FallocateFlags, fallocate};
|
||||||
|
|
||||||
use super::download_logic::download_game_bucket;
|
use super::download_logic::download_game_chunk;
|
||||||
use super::drop_data::DropData;
|
use super::drop_data::DropData;
|
||||||
|
|
||||||
static RETRY_COUNT: usize = 3;
|
static RETRY_COUNT: usize = 3;
|
||||||
|
|
||||||
const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000;
|
|
||||||
|
|
||||||
pub struct GameDownloadAgent {
|
pub struct GameDownloadAgent {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub control_flag: DownloadThreadControl,
|
pub control_flag: DownloadThreadControl,
|
||||||
buckets: Mutex<Vec<DownloadBucket>>,
|
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||||
context_map: Mutex<HashMap<String, bool>>,
|
context_map: Mutex<HashMap<String, bool>>,
|
||||||
pub manifest: Mutex<Option<DropManifest>>,
|
pub manifest: Mutex<Option<DropManifest>>,
|
||||||
pub progress: Arc<ProgressObject>,
|
pub progress: Arc<ProgressObject>,
|
||||||
@ -54,21 +50,19 @@ pub struct GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GameDownloadAgent {
|
impl GameDownloadAgent {
|
||||||
pub async fn new_from_index(
|
pub fn new_from_index(
|
||||||
id: String,
|
id: String,
|
||||||
version: String,
|
version: String,
|
||||||
target_download_dir: usize,
|
target_download_dir: usize,
|
||||||
sender: Sender<DownloadManagerSignal>,
|
sender: Sender<DownloadManagerSignal>,
|
||||||
) -> Result<Self, ApplicationDownloadError> {
|
) -> Result<Self, ApplicationDownloadError> {
|
||||||
let base_dir = {
|
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
|
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||||
|
drop(db_lock);
|
||||||
|
|
||||||
db_lock.applications.install_dirs[target_download_dir].clone()
|
Self::new(id, version, base_dir, sender)
|
||||||
};
|
|
||||||
|
|
||||||
Self::new(id, version, base_dir, sender).await
|
|
||||||
}
|
}
|
||||||
pub async fn new(
|
pub fn new(
|
||||||
id: String,
|
id: String,
|
||||||
version: String,
|
version: String,
|
||||||
base_dir: PathBuf,
|
base_dir: PathBuf,
|
||||||
@ -88,7 +82,7 @@ impl GameDownloadAgent {
|
|||||||
version,
|
version,
|
||||||
control_flag,
|
control_flag,
|
||||||
manifest: Mutex::new(None),
|
manifest: Mutex::new(None),
|
||||||
buckets: Mutex::new(Vec::new()),
|
contexts: Mutex::new(Vec::new()),
|
||||||
context_map: Mutex::new(HashMap::new()),
|
context_map: Mutex::new(HashMap::new()),
|
||||||
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
|
||||||
sender,
|
sender,
|
||||||
@ -96,7 +90,7 @@ impl GameDownloadAgent {
|
|||||||
status: Mutex::new(DownloadStatus::Queued),
|
status: Mutex::new(DownloadStatus::Queued),
|
||||||
};
|
};
|
||||||
|
|
||||||
result.ensure_manifest_exists().await?;
|
result.ensure_manifest_exists()?;
|
||||||
|
|
||||||
let required_space = result
|
let required_space = result
|
||||||
.manifest
|
.manifest
|
||||||
@ -106,7 +100,8 @@ impl GameDownloadAgent {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.values()
|
.values()
|
||||||
.map(|e| e.lengths.iter().sum::<usize>())
|
.map(|e| e.lengths.iter().sum::<usize>())
|
||||||
.sum::<usize>() as u64;
|
.sum::<usize>()
|
||||||
|
as u64;
|
||||||
|
|
||||||
let available_space = get_disk_available(data_base_dir_path)? as u64;
|
let available_space = get_disk_available(data_base_dir_path)? as u64;
|
||||||
|
|
||||||
@ -122,25 +117,26 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
// Blocking
|
// Blocking
|
||||||
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> {
|
||||||
let mut db_lock = borrow_db_mut_checked();
|
self.ensure_manifest_exists()?;
|
||||||
let status = ApplicationTransientStatus::Downloading {
|
|
||||||
version_name: self.version.clone(),
|
|
||||||
};
|
|
||||||
db_lock
|
|
||||||
.applications
|
|
||||||
.transient_statuses
|
|
||||||
.insert(self.metadata(), status.clone());
|
|
||||||
// Don't use GameStatusManager because this game isn't installed
|
|
||||||
push_game_update(app_handle, &self.metadata().id, None, (None, Some(status)));
|
|
||||||
|
|
||||||
if !self.check_manifest_exists() {
|
self.ensure_contexts()?;
|
||||||
return Err(ApplicationDownloadError::NotInitialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ensure_buckets()?;
|
|
||||||
|
|
||||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||||
|
|
||||||
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
|
db_lock.applications.transient_statuses.insert(
|
||||||
|
self.metadata(),
|
||||||
|
ApplicationTransientStatus::Downloading {
|
||||||
|
version_name: self.version.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
push_game_update(
|
||||||
|
app_handle,
|
||||||
|
&self.metadata().id,
|
||||||
|
None,
|
||||||
|
GameStatusManager::fetch_state(&self.metadata().id, &db_lock),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +147,9 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
info!("beginning download for {}...", self.metadata().id);
|
info!("beginning download for {}...", self.metadata().id);
|
||||||
|
|
||||||
let res = self.run().map_err(ApplicationDownloadError::Communication);
|
let res = self
|
||||||
|
.run()
|
||||||
|
.map_err(|()| ApplicationDownloadError::DownloadError);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"{} took {}ms to download",
|
"{} took {}ms to download",
|
||||||
@ -161,43 +159,37 @@ impl GameDownloadAgent {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_manifest_exists(&self) -> bool {
|
pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
self.manifest.lock().unwrap().is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
|
|
||||||
if self.manifest.lock().unwrap().is_some() {
|
if self.manifest.lock().unwrap().is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.download_manifest().await
|
self.download_manifest()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let header = generate_authorization_header();
|
||||||
let url = generate_url(
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = make_request(
|
||||||
|
&client,
|
||||||
&["/api/v1/client/game/manifest"],
|
&["/api/v1/client/game/manifest"],
|
||||||
&[("id", &self.id), ("version", &self.version)],
|
&[("id", &self.id), ("version", &self.version)],
|
||||||
|
|f| f.header("Authorization", header),
|
||||||
)
|
)
|
||||||
.map_err(ApplicationDownloadError::Communication)?;
|
.map_err(ApplicationDownloadError::Communication)?
|
||||||
|
|
||||||
let response = client
|
|
||||||
.get(url)
|
|
||||||
.header("Authorization", generate_authorization_header())
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
|
||||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
return Err(ApplicationDownloadError::Communication(
|
return Err(ApplicationDownloadError::Communication(
|
||||||
RemoteAccessError::ManifestDownloadFailed(
|
RemoteAccessError::ManifestDownloadFailed(
|
||||||
response.status(),
|
response.status(),
|
||||||
response.text().await.unwrap(),
|
response.text().unwrap(),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest_download: DropManifest = response.json().await.unwrap();
|
let manifest_download: DropManifest = response.json().unwrap();
|
||||||
|
|
||||||
if let Ok(mut manifest) = self.manifest.lock() {
|
if let Ok(mut manifest) = self.manifest.lock() {
|
||||||
*manifest = Some(manifest_download);
|
*manifest = Some(manifest_download);
|
||||||
@ -209,23 +201,20 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
// Sets it up for both download and validate
|
// Sets it up for both download and validate
|
||||||
fn setup_progress(&self) {
|
fn setup_progress(&self) {
|
||||||
let buckets = self.buckets.lock().unwrap();
|
let contexts = self.contexts.lock().unwrap();
|
||||||
|
|
||||||
let chunk_count = buckets.iter().map(|e| e.drops.len()).sum();
|
let length = contexts.len();
|
||||||
|
|
||||||
let total_length = buckets
|
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
|
||||||
.iter()
|
|
||||||
.map(|bucket| bucket.drops.iter().map(|e| e.length).sum::<usize>())
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
self.progress.set_max(total_length);
|
self.progress.set_max(chunk_count);
|
||||||
self.progress.set_size(chunk_count);
|
self.progress.set_size(length);
|
||||||
self.progress.reset();
|
self.progress.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
if self.buckets.lock().unwrap().is_empty() {
|
if self.contexts.lock().unwrap().is_empty() {
|
||||||
self.generate_buckets()?;
|
self.generate_contexts()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
|
*self.context_map.lock().unwrap() = self.dropdata.get_contexts();
|
||||||
@ -233,22 +222,14 @@ impl GameDownloadAgent {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> {
|
pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> {
|
||||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||||
let game_id = self.id.clone();
|
let game_id = self.id.clone();
|
||||||
|
|
||||||
|
let mut contexts = Vec::new();
|
||||||
let base_path = Path::new(&self.dropdata.base_path);
|
let base_path = Path::new(&self.dropdata.base_path);
|
||||||
create_dir_all(base_path).unwrap();
|
create_dir_all(base_path).unwrap();
|
||||||
|
|
||||||
let mut buckets = Vec::new();
|
|
||||||
|
|
||||||
let mut current_bucket = DownloadBucket {
|
|
||||||
game_id: game_id.clone(),
|
|
||||||
version: self.version.clone(),
|
|
||||||
drops: Vec::new(),
|
|
||||||
};
|
|
||||||
let mut current_bucket_size = 0;
|
|
||||||
|
|
||||||
for (raw_path, chunk) in manifest {
|
for (raw_path, chunk) in manifest {
|
||||||
let path = base_path.join(Path::new(&raw_path));
|
let path = base_path.join(Path::new(&raw_path));
|
||||||
|
|
||||||
@ -263,79 +244,42 @@ impl GameDownloadAgent {
|
|||||||
.truncate(false)
|
.truncate(false)
|
||||||
.open(path.clone())
|
.open(path.clone())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut file_running_offset = 0;
|
let mut running_offset = 0;
|
||||||
|
|
||||||
for (index, length) in chunk.lengths.iter().enumerate() {
|
for (index, length) in chunk.lengths.iter().enumerate() {
|
||||||
let drop = DownloadDrop {
|
contexts.push(DropDownloadContext {
|
||||||
filename: raw_path.to_string(),
|
file_name: raw_path.to_string(),
|
||||||
start: file_running_offset,
|
version: chunk.version_name.to_string(),
|
||||||
length: *length,
|
offset: running_offset,
|
||||||
checksum: chunk.checksums[index].clone(),
|
|
||||||
permissions: chunk.permissions,
|
|
||||||
path: path.clone(),
|
|
||||||
index,
|
index,
|
||||||
};
|
game_id: game_id.to_string(),
|
||||||
file_running_offset += *length;
|
path: path.clone(),
|
||||||
|
checksum: chunk.checksums[index].clone(),
|
||||||
if *length >= TARGET_BUCKET_SIZE {
|
length: *length,
|
||||||
// They get their own bucket
|
permissions: chunk.permissions,
|
||||||
|
|
||||||
buckets.push(DownloadBucket {
|
|
||||||
game_id: game_id.clone(),
|
|
||||||
version: self.version.clone(),
|
|
||||||
drops: vec![drop],
|
|
||||||
});
|
});
|
||||||
|
running_offset += *length as u64;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_bucket_size + *length >= TARGET_BUCKET_SIZE
|
|
||||||
&& !current_bucket.drops.is_empty()
|
|
||||||
{
|
|
||||||
// Move current bucket into list and make a new one
|
|
||||||
buckets.push(current_bucket);
|
|
||||||
current_bucket = DownloadBucket {
|
|
||||||
game_id: game_id.clone(),
|
|
||||||
version: self.version.clone(),
|
|
||||||
drops: Vec::new(),
|
|
||||||
};
|
|
||||||
current_bucket_size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_bucket.drops.push(drop);
|
|
||||||
current_bucket_size += *length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if file_running_offset > 0 && !already_exists {
|
if running_offset > 0 && !already_exists {
|
||||||
let _ = fallocate(file, FallocateFlags::empty(), 0, file_running_offset as u64);
|
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let existing_contexts = self.dropdata.get_completed_contexts();
|
||||||
if !current_bucket.drops.is_empty() {
|
|
||||||
buckets.push(current_bucket);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("buckets: {}", buckets.len());
|
|
||||||
|
|
||||||
let existing_contexts = self.dropdata.get_contexts();
|
|
||||||
self.dropdata.set_contexts(
|
self.dropdata.set_contexts(
|
||||||
&buckets
|
&contexts
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|x| x.drops.iter().map(|v| v.checksum.clone()))
|
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
|
||||||
.map(|x| {
|
|
||||||
let contains = existing_contexts.get(&x).unwrap_or(&false);
|
|
||||||
(x, *contains)
|
|
||||||
})
|
|
||||||
.collect::<Vec<(String, bool)>>(),
|
.collect::<Vec<(String, bool)>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
*self.buckets.lock().unwrap() = buckets;
|
*self.contexts.lock().unwrap() = contexts;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self) -> Result<bool, RemoteAccessError> {
|
fn run(&self) -> Result<bool, ()> {
|
||||||
self.setup_progress();
|
self.setup_progress();
|
||||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||||
|
|
||||||
@ -351,81 +295,78 @@ impl GameDownloadAgent {
|
|||||||
let completed_contexts = Arc::new(boxcar::Vec::new());
|
let completed_contexts = Arc::new(boxcar::Vec::new());
|
||||||
let completed_indexes_loop_arc = completed_contexts.clone();
|
let completed_indexes_loop_arc = completed_contexts.clone();
|
||||||
|
|
||||||
let download_context = DROP_CLIENT_SYNC
|
let contexts = self.contexts.lock().unwrap();
|
||||||
.post(generate_url(&["/api/v2/client/context"], &[]).unwrap())
|
|
||||||
.json(&ManifestBody {
|
|
||||||
game: self.id.clone(),
|
|
||||||
version: self.version.clone(),
|
|
||||||
})
|
|
||||||
.header("Authorization", generate_authorization_header())
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if download_context.status() != 200 {
|
|
||||||
return Err(RemoteAccessError::InvalidResponse(download_context.json()?));
|
|
||||||
}
|
|
||||||
|
|
||||||
let download_context = &download_context.json::<DownloadContext>()?;
|
|
||||||
|
|
||||||
info!("download context: {}", download_context.context);
|
|
||||||
|
|
||||||
let buckets = self.buckets.lock().unwrap();
|
|
||||||
pool.scope(|scope| {
|
pool.scope(|scope| {
|
||||||
|
let client = &DROP_CLIENT_SYNC.clone();
|
||||||
let context_map = self.context_map.lock().unwrap();
|
let context_map = self.context_map.lock().unwrap();
|
||||||
for (index, bucket) in buckets.iter().enumerate() {
|
for (index, context) in contexts.iter().enumerate() {
|
||||||
let mut bucket = (*bucket).clone();
|
let client = client.clone();
|
||||||
let completed_contexts = completed_indexes_loop_arc.clone();
|
let completed_indexes = completed_indexes_loop_arc.clone();
|
||||||
|
|
||||||
let progress = self.progress.get(index);
|
let progress = self.progress.get(index);
|
||||||
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
|
||||||
|
|
||||||
// If we've done this one already, skip it
|
// If we've done this one already, skip it
|
||||||
// Note to future DecDuck, DropData gets loaded into context_map
|
// Note to future DecDuck, DropData gets loaded into context_map
|
||||||
let todo_drops = bucket
|
if let Some(v) = context_map.get(&context.checksum)
|
||||||
.drops
|
&& *v
|
||||||
.into_iter()
|
{
|
||||||
.filter(|e| {
|
progress_handle.skip(context.length);
|
||||||
let todo = !*context_map.get(&e.checksum).unwrap_or(&false);
|
|
||||||
if !todo {
|
|
||||||
progress_handle.skip(e.length);
|
|
||||||
}
|
|
||||||
todo
|
|
||||||
})
|
|
||||||
.collect::<Vec<DownloadDrop>>();
|
|
||||||
|
|
||||||
if todo_drops.is_empty() {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
}
|
||||||
|
|
||||||
bucket.drops = todo_drops;
|
|
||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
|
let request = match make_request(
|
||||||
|
&client,
|
||||||
|
&["/api/v1/client/chunk"],
|
||||||
|
&[
|
||||||
|
("id", &context.game_id),
|
||||||
|
("version", &context.version),
|
||||||
|
("name", &context.file_name),
|
||||||
|
("chunk", &context.index.to_string()),
|
||||||
|
],
|
||||||
|
|r| r,
|
||||||
|
) {
|
||||||
|
Ok(request) => request,
|
||||||
|
Err(e) => {
|
||||||
|
sender
|
||||||
|
.send(DownloadManagerSignal::Error(
|
||||||
|
ApplicationDownloadError::Communication(e),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
scope.spawn(move |_| {
|
scope.spawn(move |_| {
|
||||||
// 3 attempts
|
// 3 attempts
|
||||||
for i in 0..RETRY_COUNT {
|
for i in 0..RETRY_COUNT {
|
||||||
let loop_progress_handle = progress_handle.clone();
|
let loop_progress_handle = progress_handle.clone();
|
||||||
match download_game_bucket(
|
match download_game_chunk(
|
||||||
&bucket,
|
context,
|
||||||
download_context,
|
|
||||||
&self.control_flag,
|
&self.control_flag,
|
||||||
loop_progress_handle,
|
loop_progress_handle,
|
||||||
|
request.try_clone().unwrap(),
|
||||||
) {
|
) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
for drop in bucket.drops {
|
completed_indexes.push(context.checksum.clone());
|
||||||
completed_contexts.push(drop.checksum);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Ok(false) => return,
|
Ok(false) => return,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("game download agent error: {e}");
|
warn!("game download agent error: {e}");
|
||||||
|
|
||||||
let retry = matches!(
|
let retry = match &e {
|
||||||
&e,
|
ApplicationDownloadError::Communication(
|
||||||
ApplicationDownloadError::Communication(_)
|
_remote_access_error,
|
||||||
| ApplicationDownloadError::Checksum
|
) => true,
|
||||||
| ApplicationDownloadError::Lock
|
ApplicationDownloadError::Checksum => true,
|
||||||
);
|
ApplicationDownloadError::Lock => true,
|
||||||
|
ApplicationDownloadError::IoError(_error_kind) => false,
|
||||||
|
ApplicationDownloadError::DownloadError => false,
|
||||||
|
ApplicationDownloadError::DiskFull(_, _) => false,
|
||||||
|
};
|
||||||
|
|
||||||
if i == RETRY_COUNT - 1 || !retry {
|
if i == RETRY_COUNT - 1 || !retry {
|
||||||
warn!("retry logic failed, not re-attempting.");
|
warn!("retry logic failed, not re-attempting.");
|
||||||
@ -449,14 +390,14 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
context_map_lock.values().filter(|x| **x).count()
|
context_map_lock.values().filter(|x| **x).count()
|
||||||
};
|
};
|
||||||
|
|
||||||
let context_map_lock = self.context_map.lock().unwrap();
|
let context_map_lock = self.context_map.lock().unwrap();
|
||||||
let contexts = buckets
|
let contexts = contexts
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|x| x.drops.iter().map(|e| e.checksum.clone()))
|
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
let completed = context_map_lock.get(&x).unwrap_or(&false);
|
(
|
||||||
(x, *completed)
|
x.checksum.clone(),
|
||||||
|
context_map_lock.get(&x.checksum).copied().unwrap_or(false),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<(String, bool)>>();
|
.collect::<Vec<(String, bool)>>();
|
||||||
drop(context_map_lock);
|
drop(context_map_lock);
|
||||||
@ -467,11 +408,10 @@ impl GameDownloadAgent {
|
|||||||
// If there are any contexts left which are false
|
// If there are any contexts left which are false
|
||||||
if !contexts.iter().all(|x| x.1) {
|
if !contexts.iter().all(|x| x.1) {
|
||||||
info!(
|
info!(
|
||||||
"download agent for {} exited without completing ({}/{}) ({} buckets)",
|
"download agent for {} exited without completing ({}/{})",
|
||||||
self.id.clone(),
|
self.id.clone(),
|
||||||
completed_lock_len,
|
completed_lock_len,
|
||||||
contexts.len(),
|
contexts.len(),
|
||||||
buckets.len()
|
|
||||||
);
|
);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@ -484,30 +424,31 @@ impl GameDownloadAgent {
|
|||||||
|
|
||||||
self.control_flag.set(DownloadThreadControlFlag::Go);
|
self.control_flag.set(DownloadThreadControlFlag::Go);
|
||||||
|
|
||||||
let status = ApplicationTransientStatus::Validating {
|
|
||||||
version_name: self.version.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut db_lock = borrow_db_mut_checked();
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
db_lock
|
db_lock.applications.transient_statuses.insert(
|
||||||
.applications
|
self.metadata(),
|
||||||
.transient_statuses
|
ApplicationTransientStatus::Validating {
|
||||||
.insert(self.metadata(), status.clone());
|
version_name: self.version.clone(),
|
||||||
push_game_update(app_handle, &self.metadata().id, None, (None, Some(status)));
|
},
|
||||||
|
);
|
||||||
|
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> {
|
pub fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
|
||||||
self.setup_validate(app_handle);
|
self.setup_validate(app_handle);
|
||||||
|
|
||||||
let buckets = self.buckets.lock().unwrap();
|
let contexts = self.contexts.lock().unwrap();
|
||||||
let contexts: Vec<DropValidateContext> = buckets
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|e| -> Vec<DropValidateContext> { e.into() })
|
|
||||||
.collect();
|
|
||||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||||
|
|
||||||
info!("{} validation contexts", contexts.len());
|
debug!(
|
||||||
|
"validating game: {} with {} threads",
|
||||||
|
self.dropdata.game_id, max_download_threads
|
||||||
|
);
|
||||||
let pool = ThreadPoolBuilder::new()
|
let pool = ThreadPoolBuilder::new()
|
||||||
.num_threads(max_download_threads)
|
.num_threads(max_download_threads)
|
||||||
.build()
|
.build()
|
||||||
@ -608,13 +549,6 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
.applications
|
.applications
|
||||||
.transient_statuses
|
.transient_statuses
|
||||||
.remove(&self.metadata());
|
.remove(&self.metadata());
|
||||||
|
|
||||||
push_game_update(
|
|
||||||
app_handle,
|
|
||||||
&self.id,
|
|
||||||
None,
|
|
||||||
GameStatusManager::fetch_state(&self.id, &handle),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||||
|
|||||||
@ -5,48 +5,48 @@ use crate::download_manager::util::progress_object::ProgressHandle;
|
|||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::drop_server_error::DropServerError;
|
use crate::error::drop_server_error::DropServerError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop};
|
use crate::games::downloads::manifest::DropDownloadContext;
|
||||||
use crate::remote::auth::generate_authorization_header;
|
use crate::remote::auth::generate_authorization_header;
|
||||||
use crate::remote::requests::generate_url;
|
use http::response;
|
||||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
use log::{debug, info, warn};
|
||||||
use log::{info, warn};
|
|
||||||
use md5::{Context, Digest};
|
use md5::{Context, Digest};
|
||||||
use reqwest::blocking::Response;
|
use reqwest::blocking::{RequestBuilder, Response};
|
||||||
|
|
||||||
use std::fs::{Permissions, set_permissions};
|
use std::fs::{Permissions, set_permissions};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::ops::Sub;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::sync::Arc;
|
use std::thread;
|
||||||
|
use std::time::Instant;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{File, OpenOptions},
|
fs::{File, OpenOptions},
|
||||||
io::{self, BufWriter, Seek, SeekFrom, Write},
|
io::{self, BufWriter, Seek, SeekFrom, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
static MAX_PACKET_LENGTH: usize = 4096 * 4;
|
|
||||||
|
|
||||||
pub struct DropWriter<W: Write> {
|
pub struct DropWriter<W: Write> {
|
||||||
hasher: Context,
|
hasher: Context,
|
||||||
destination: BufWriter<W>,
|
destination: BufWriter<W>,
|
||||||
progress: ProgressHandle,
|
progress: ProgressHandle,
|
||||||
}
|
}
|
||||||
impl DropWriter<File> {
|
impl DropWriter<File> {
|
||||||
fn new(path: PathBuf, progress: ProgressHandle) -> Result<Self, io::Error> {
|
fn new(path: PathBuf, progress: ProgressHandle) -> Self {
|
||||||
let destination = OpenOptions::new()
|
let destination = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(false)
|
.truncate(false)
|
||||||
.open(&path)?;
|
.open(&path)
|
||||||
Ok(Self {
|
.unwrap();
|
||||||
destination: BufWriter::with_capacity(1024 * 1024, destination),
|
Self {
|
||||||
|
destination: BufWriter::with_capacity(1 * 1024 * 1024, destination),
|
||||||
hasher: Context::new(),
|
hasher: Context::new(),
|
||||||
progress,
|
progress,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut self) -> io::Result<Digest> {
|
fn finish(mut self) -> io::Result<Digest> {
|
||||||
self.flush()?;
|
self.flush().unwrap();
|
||||||
Ok(self.hasher.compute())
|
Ok(self.hasher.compute())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,76 +76,50 @@ impl Seek for DropWriter<File> {
|
|||||||
|
|
||||||
pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
||||||
pub source: R,
|
pub source: R,
|
||||||
pub drops: Vec<DownloadDrop>,
|
pub destination: DropWriter<W>,
|
||||||
pub destination: Vec<DropWriter<W>>,
|
|
||||||
pub control_flag: &'a DownloadThreadControl,
|
pub control_flag: &'a DownloadThreadControl,
|
||||||
|
pub size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Seek for DropDownloadPipeline<'a, Response, File> {
|
||||||
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
|
self.destination.seek(pos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||||
fn new(
|
fn new(
|
||||||
source: Response,
|
source: Response,
|
||||||
drops: Vec<DownloadDrop>,
|
destination: PathBuf,
|
||||||
control_flag: &'a DownloadThreadControl,
|
control_flag: &'a DownloadThreadControl,
|
||||||
progress: ProgressHandle,
|
progress: ProgressHandle,
|
||||||
) -> Result<Self, io::Error> {
|
size: usize,
|
||||||
Ok(Self {
|
) -> Self {
|
||||||
|
Self {
|
||||||
source,
|
source,
|
||||||
destination: drops
|
destination: DropWriter::new(destination, progress),
|
||||||
.iter()
|
|
||||||
.map(|drop| DropWriter::new(drop.path.clone(), progress.clone()))
|
|
||||||
.try_collect()?,
|
|
||||||
drops,
|
|
||||||
control_flag,
|
control_flag,
|
||||||
})
|
size,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy(&mut self) -> Result<bool, io::Error> {
|
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||||
let mut copy_buffer = [0u8; MAX_PACKET_LENGTH];
|
io::copy(&mut self.source, &mut self.destination).unwrap();
|
||||||
for (index, drop) in self.drops.iter().enumerate() {
|
|
||||||
let destination = self
|
|
||||||
.destination
|
|
||||||
.get_mut(index)
|
|
||||||
.ok_or(io::Error::other("no destination"))
|
|
||||||
.unwrap();
|
|
||||||
let mut remaining = drop.length;
|
|
||||||
if drop.start != 0 {
|
|
||||||
destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?;
|
|
||||||
}
|
|
||||||
loop {
|
|
||||||
let size = MAX_PACKET_LENGTH.min(remaining);
|
|
||||||
self.source.read_exact(&mut copy_buffer[0..size])?;
|
|
||||||
remaining -= size;
|
|
||||||
|
|
||||||
destination.write_all(©_buffer[0..size])?;
|
|
||||||
|
|
||||||
if remaining == 0 {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(self) -> Result<Vec<Digest>, io::Error> {
|
fn finish(self) -> Result<Digest, io::Error> {
|
||||||
let checksums = self
|
let checksum = self.destination.finish()?;
|
||||||
.destination
|
Ok(checksum)
|
||||||
.into_iter()
|
|
||||||
.map(|e| e.finish())
|
|
||||||
.try_collect()?;
|
|
||||||
Ok(checksums)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_game_bucket(
|
pub fn download_game_chunk(
|
||||||
bucket: &DownloadBucket,
|
ctx: &DropDownloadContext,
|
||||||
ctx: &DownloadContext,
|
|
||||||
control_flag: &DownloadThreadControl,
|
control_flag: &DownloadThreadControl,
|
||||||
progress: ProgressHandle,
|
progress: ProgressHandle,
|
||||||
|
request: RequestBuilder,
|
||||||
) -> Result<bool, ApplicationDownloadError> {
|
) -> Result<bool, ApplicationDownloadError> {
|
||||||
// If we're paused
|
// If we're paused
|
||||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||||
@ -153,26 +127,23 @@ pub fn download_game_bucket(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
debug!("started chunk {}", ctx.checksum);
|
||||||
|
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
|
let header_time = start.elapsed();
|
||||||
|
|
||||||
let url = generate_url(&["/api/v2/client/chunk"], &[])
|
let response = request
|
||||||
.map_err(ApplicationDownloadError::Communication)?;
|
|
||||||
|
|
||||||
let body = ChunkBody::create(ctx, &bucket.drops);
|
|
||||||
|
|
||||||
let response = DROP_CLIENT_SYNC
|
|
||||||
.post(url)
|
|
||||||
.json(&body)
|
|
||||||
.header("Authorization", header)
|
.header("Authorization", header)
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
|
||||||
|
|
||||||
|
let response_time = start.elapsed();
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
info!("chunk request got status code: {}", response.status());
|
debug!("chunk request got status code: {}", response.status());
|
||||||
let raw_res = response.text().map_err(|e| {
|
let raw_res = response.text().unwrap();
|
||||||
ApplicationDownloadError::Communication(RemoteAccessError::FetchError(e.into()))
|
|
||||||
})?;
|
|
||||||
info!("{raw_res}");
|
|
||||||
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
|
if let Ok(err) = serde_json::from_str::<DropServerError>(&raw_res) {
|
||||||
return Err(ApplicationDownloadError::Communication(
|
return Err(ApplicationDownloadError::Communication(
|
||||||
RemoteAccessError::InvalidResponse(err),
|
RemoteAccessError::InvalidResponse(err),
|
||||||
@ -183,65 +154,78 @@ pub fn download_game_bucket(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let lengths = response
|
let content_length = response.content_length();
|
||||||
.headers()
|
if content_length.is_none() {
|
||||||
.get("Content-Lengths")
|
warn!("recieved 0 length content from server");
|
||||||
.ok_or(ApplicationDownloadError::Communication(
|
return Err(ApplicationDownloadError::Communication(
|
||||||
RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()),
|
RemoteAccessError::InvalidResponse(response.json().unwrap()),
|
||||||
))?
|
));
|
||||||
.to_str()
|
}
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for (i, raw_length) in lengths.split(",").enumerate() {
|
let length = content_length.unwrap().try_into().unwrap();
|
||||||
let length = raw_length.parse::<usize>().unwrap_or(0);
|
|
||||||
let Some(drop) = bucket.drops.get(i) else {
|
if length != ctx.length {
|
||||||
warn!(
|
|
||||||
"invalid number of Content-Lengths recieved: {i}, {lengths}"
|
|
||||||
);
|
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
|
||||||
};
|
|
||||||
if drop.length != length {
|
|
||||||
warn!(
|
|
||||||
"for {}, expected {}, got {} ({})",
|
|
||||||
drop.filename, drop.length, raw_length, length
|
|
||||||
);
|
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
return Err(ApplicationDownloadError::DownloadError);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let pipeline_start = start.elapsed();
|
||||||
|
|
||||||
let mut pipeline =
|
let mut pipeline =
|
||||||
DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress)
|
DropDownloadPipeline::new(response, ctx.path.clone(), control_flag, progress, length);
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
|
||||||
|
if ctx.offset != 0 {
|
||||||
|
pipeline
|
||||||
|
.seek(SeekFrom::Start(ctx.offset))
|
||||||
|
.expect("Failed to seek to file offset");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pipeline_setup = start.elapsed();
|
||||||
|
|
||||||
let completed = pipeline
|
let completed = pipeline
|
||||||
.copy()
|
.copy()
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||||
if !completed {
|
if !completed {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pipeline_finish = start.elapsed();
|
||||||
|
|
||||||
// If we complete the file, set the permissions (if on Linux)
|
// If we complete the file, set the permissions (if on Linux)
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
for drop in bucket.drops.iter() {
|
let permissions = Permissions::from_mode(ctx.permissions);
|
||||||
let permissions = Permissions::from_mode(drop.permissions);
|
set_permissions(ctx.path.clone(), permissions).unwrap();
|
||||||
set_permissions(drop.path.clone(), permissions)
|
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let checksums = pipeline
|
let checksum = pipeline
|
||||||
.finish()
|
.finish()
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
|
||||||
|
|
||||||
for (index, drop) in bucket.drops.iter().enumerate() {
|
let checksum_finish = start.elapsed();
|
||||||
let res = hex::encode(**checksums.get(index).unwrap());
|
|
||||||
if res != drop.checksum {
|
let res = hex::encode(checksum.0);
|
||||||
warn!("context didn't match... doing nothing because we will validate later.");
|
if res != ctx.checksum {
|
||||||
// return Ok(false);
|
return Err(ApplicationDownloadError::Checksum);
|
||||||
// return Err(ApplicationDownloadError::Checksum);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let header_update = header_time.as_millis();
|
||||||
|
let response_update = response_time.sub(header_time).as_millis();
|
||||||
|
let pipeline_start_update = pipeline_start.sub(response_time).as_millis();
|
||||||
|
let pipeline_setup_update = pipeline_setup.sub(pipeline_start).as_millis();
|
||||||
|
let pipeline_finish_update = pipeline_finish.sub(pipeline_setup).as_millis();
|
||||||
|
let checksum_update = checksum_finish.sub(pipeline_finish).as_millis();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"\nheader: {}\nresponse: {}\npipeline start: {}\npipeline setup: {}\npipeline finish: {}\nchecksum finish: {}",
|
||||||
|
header_update,
|
||||||
|
response_update,
|
||||||
|
pipeline_start_update,
|
||||||
|
pipeline_setup_update,
|
||||||
|
pipeline_finish_update,
|
||||||
|
checksum_update
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!("finished chunk {}", ctx.checksum);
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,14 @@ impl DropData {
|
|||||||
pub fn set_context(&self, context: String, state: bool) {
|
pub fn set_context(&self, context: String, state: bool) {
|
||||||
self.contexts.lock().unwrap().entry(context).insert_entry(state);
|
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 })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
pub fn get_contexts(&self) -> HashMap<String, bool> {
|
||||||
self.contexts.lock().unwrap().clone()
|
self.contexts.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,65 +2,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
// Drops go in buckets
|
|
||||||
pub struct DownloadDrop {
|
|
||||||
pub index: usize,
|
|
||||||
pub filename: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub start: usize,
|
|
||||||
pub length: usize,
|
|
||||||
pub checksum: String,
|
|
||||||
pub permissions: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DownloadBucket {
|
|
||||||
pub game_id: String,
|
|
||||||
pub version: String,
|
|
||||||
pub drops: Vec<DownloadDrop>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct DownloadContext {
|
|
||||||
pub context: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ChunkBodyFile {
|
|
||||||
filename: String,
|
|
||||||
chunk_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ChunkBody {
|
|
||||||
pub context: String,
|
|
||||||
pub files: Vec<ChunkBodyFile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct ManifestBody {
|
|
||||||
pub game: String,
|
|
||||||
pub version: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChunkBody {
|
|
||||||
pub fn create(context: &DownloadContext, drops: &[DownloadDrop]) -> ChunkBody {
|
|
||||||
Self {
|
|
||||||
context: context.context.clone(),
|
|
||||||
files: drops
|
|
||||||
.iter()
|
|
||||||
.map(|e| ChunkBodyFile {
|
|
||||||
filename: e.filename.clone(),
|
|
||||||
chunk_index: e.index,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type DropManifest = HashMap<String, DropChunk>;
|
pub type DropManifest = HashMap<String, DropChunk>;
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -73,26 +14,14 @@ pub struct DropChunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct DropValidateContext {
|
pub struct DropDownloadContext {
|
||||||
|
pub file_name: String,
|
||||||
|
pub version: String,
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub offset: usize,
|
pub offset: u64,
|
||||||
|
pub game_id: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub checksum: String,
|
pub checksum: String,
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
}
|
pub permissions: u32,
|
||||||
|
|
||||||
impl From<DownloadBucket> for Vec<DropValidateContext> {
|
|
||||||
fn from(value: DownloadBucket) -> Self {
|
|
||||||
value
|
|
||||||
.drops
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| DropValidateContext {
|
|
||||||
index: e.index,
|
|
||||||
offset: e.start,
|
|
||||||
path: e.path,
|
|
||||||
checksum: e.checksum,
|
|
||||||
length: e.length,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,22 +7,24 @@ use log::debug;
|
|||||||
use md5::Context;
|
use md5::Context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
download_manager::util::{
|
download_manager::
|
||||||
|
util::{
|
||||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||||
progress_object::ProgressHandle,
|
progress_object::ProgressHandle,
|
||||||
},
|
}
|
||||||
|
,
|
||||||
error::application_download_error::ApplicationDownloadError,
|
error::application_download_error::ApplicationDownloadError,
|
||||||
games::downloads::manifest::DropValidateContext,
|
games::downloads::manifest::DropDownloadContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn validate_game_chunk(
|
pub fn validate_game_chunk(
|
||||||
ctx: &DropValidateContext,
|
ctx: &DropDownloadContext,
|
||||||
control_flag: &DownloadThreadControl,
|
control_flag: &DownloadThreadControl,
|
||||||
progress: ProgressHandle,
|
progress: ProgressHandle,
|
||||||
) -> Result<bool, ApplicationDownloadError> {
|
) -> Result<bool, ApplicationDownloadError> {
|
||||||
debug!(
|
debug!(
|
||||||
"Starting chunk validation {}, {}, {} #{}",
|
"Starting chunk validation {}, {}, {} #{}",
|
||||||
ctx.path.display(), ctx.index, ctx.offset, ctx.checksum
|
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
|
||||||
);
|
);
|
||||||
// If we're paused
|
// If we're paused
|
||||||
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
if control_flag.get() == DownloadThreadControlFlag::Stop {
|
||||||
@ -36,7 +38,7 @@ pub fn validate_game_chunk(
|
|||||||
|
|
||||||
if ctx.offset != 0 {
|
if ctx.offset != 0 {
|
||||||
source
|
source
|
||||||
.seek(SeekFrom::Start(ctx.offset.try_into().unwrap()))
|
.seek(SeekFrom::Start(ctx.offset))
|
||||||
.expect("Failed to seek to file offset");
|
.expect("Failed to seek to file offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,15 +14,13 @@ use crate::database::models::data::{
|
|||||||
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||||
};
|
};
|
||||||
use crate::download_manager::download_manager_frontend::DownloadStatus;
|
use crate::download_manager::download_manager_frontend::DownloadStatus;
|
||||||
use crate::error::drop_server_error::DropServerError;
|
|
||||||
use crate::error::library_error::LibraryError;
|
use crate::error::library_error::LibraryError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
use crate::remote::auth::generate_authorization_header;
|
use crate::remote::auth::generate_authorization_header;
|
||||||
use crate::remote::cache::cache_object_db;
|
use crate::remote::cache::cache_object_db;
|
||||||
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||||
use crate::remote::requests::generate_url;
|
use crate::remote::requests::make_request;
|
||||||
use crate::remote::utils::DROP_CLIENT_ASYNC;
|
|
||||||
use crate::remote::utils::DROP_CLIENT_SYNC;
|
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||||
use bitcode::{Decode, Encode};
|
use bitcode::{Decode, Encode};
|
||||||
|
|
||||||
@ -78,27 +76,24 @@ pub struct StatsUpdateEvent {
|
|||||||
pub time: usize,
|
pub time: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_library_logic(
|
pub fn fetch_library_logic(
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let header = generate_authorization_header();
|
||||||
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
|
|
||||||
let response = client
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
.get(response)
|
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
|
||||||
.header("Authorization", generate_authorization_header())
|
f.header("Authorization", header)
|
||||||
.send()
|
})?
|
||||||
.await?;
|
.send()?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap_or(DropServerError {
|
let err = response.json().unwrap();
|
||||||
status_code: 500,
|
|
||||||
status_message: "Invalid response from server.".to_owned(),
|
|
||||||
});
|
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut games: Vec<Game> = response.json().await?;
|
let mut games: Vec<Game> = response.json()?;
|
||||||
|
|
||||||
let mut handle = state.lock().unwrap();
|
let mut handle = state.lock().unwrap();
|
||||||
|
|
||||||
@ -140,32 +135,27 @@ pub async fn fetch_library_logic(
|
|||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
pub async fn fetch_library_logic_offline(
|
pub fn fetch_library_logic_offline(
|
||||||
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
_state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
let mut games: Vec<Game> = get_cached_object("library")?;
|
let mut games: Vec<Game> = get_cached_object("library")?;
|
||||||
|
|
||||||
let db_handle = borrow_db_checked();
|
let db_handle = borrow_db_checked();
|
||||||
|
|
||||||
games.retain(|game| {
|
games.retain(|game| {
|
||||||
matches!(
|
db_handle
|
||||||
&db_handle
|
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.installed_game_version
|
||||||
.get(&game.id)
|
.contains_key(&game.id)
|
||||||
.unwrap_or(&GameDownloadStatus::Remote {}),
|
|
||||||
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
pub async fn fetch_game_logic(
|
pub fn fetch_game_logic(
|
||||||
id: String,
|
id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let version = {
|
let mut state_handle = state.lock().unwrap();
|
||||||
let state_handle = state.lock().unwrap();
|
|
||||||
|
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
|
|
||||||
@ -194,35 +184,24 @@ pub async fn fetch_game_logic(
|
|||||||
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
|
drop(db_lock);
|
||||||
|
|
||||||
version
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
};
|
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
|
||||||
|
r.header("Authorization", generate_authorization_header())
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
})?
|
||||||
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
|
.send()?;
|
||||||
let response = client
|
|
||||||
.get(response)
|
|
||||||
.header("Authorization", generate_authorization_header())
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status() == 404 {
|
if response.status() == 404 {
|
||||||
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
|
|
||||||
if let Ok(fetch_data) = offline_fetch {
|
|
||||||
return Ok(fetch_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(RemoteAccessError::GameNotFound(id));
|
return Err(RemoteAccessError::GameNotFound(id));
|
||||||
}
|
}
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().unwrap();
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let game: Game = response.json().await?;
|
let game: Game = response.json()?;
|
||||||
|
|
||||||
let mut state_handle = state.lock().unwrap();
|
|
||||||
state_handle.games.insert(id.clone(), game.clone());
|
state_handle.games.insert(id.clone(), game.clone());
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
@ -248,20 +227,24 @@ pub async fn fetch_game_logic(
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_game_logic_offline(
|
pub fn fetch_game_logic_offline(
|
||||||
id: String,
|
id: String,
|
||||||
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
_state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let db_handle = borrow_db_checked();
|
let db_handle = borrow_db_checked();
|
||||||
let metadata_option = db_handle.applications.installed_game_version.get(&id);
|
let metadata_option = db_handle.applications.installed_game_version.get(&id);
|
||||||
let version = match metadata_option {
|
let version = match metadata_option {
|
||||||
None => None,
|
None => None,
|
||||||
Some(metadata) => db_handle
|
Some(metadata) => Some(
|
||||||
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.game_versions
|
.game_versions
|
||||||
.get(&metadata.id)
|
.get(&metadata.id)
|
||||||
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
|
.unwrap()
|
||||||
.cloned(),
|
.get(metadata.version.as_ref().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
let status = GameStatusManager::fetch_state(&id, &db_handle);
|
||||||
@ -276,26 +259,27 @@ pub async fn fetch_game_logic_offline(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_game_version_options_logic(
|
pub fn fetch_game_verion_options_logic(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
|
||||||
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
|
let response = make_request(
|
||||||
let response = client
|
&client,
|
||||||
.get(response)
|
&["/api/v1/client/game/versions"],
|
||||||
.header("Authorization", generate_authorization_header())
|
&[("id", &game_id)],
|
||||||
.send()
|
|r| r.header("Authorization", generate_authorization_header()),
|
||||||
.await?;
|
)?
|
||||||
|
.send()?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap();
|
let err = response.json().unwrap();
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Vec<GameVersion> = response.json().await?;
|
let data: Vec<GameVersion> = response.json()?;
|
||||||
|
|
||||||
let state_lock = state.lock().unwrap();
|
let state_lock = state.lock().unwrap();
|
||||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||||
@ -362,7 +346,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.transient_statuses
|
.transient_statuses
|
||||||
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
.entry(meta.clone())
|
||||||
|
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||||
|
|
||||||
push_game_update(
|
push_game_update(
|
||||||
app_handle,
|
app_handle,
|
||||||
@ -396,7 +381,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.transient_statuses
|
.transient_statuses
|
||||||
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
|
.entry(meta.clone())
|
||||||
|
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
|
||||||
|
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
@ -414,7 +400,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
.game_statuses
|
.game_statuses
|
||||||
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
|
.entry(meta.id.clone())
|
||||||
|
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||||
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
let _ = db_handle.applications.transient_statuses.remove(&meta);
|
||||||
|
|
||||||
push_game_update(
|
push_game_update(
|
||||||
@ -426,6 +413,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
|||||||
|
|
||||||
debug!("uninstalled game id {}", &meta.id);
|
debug!("uninstalled game id {}", &meta.id);
|
||||||
app_handle.emit("update_library", ()).unwrap();
|
app_handle.emit("update_library", ()).unwrap();
|
||||||
|
|
||||||
|
drop(db_handle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -451,17 +440,18 @@ pub fn on_game_complete(
|
|||||||
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let client = DROP_CLIENT_SYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
let response = generate_url(
|
let response = make_request(
|
||||||
|
&client,
|
||||||
&["/api/v1/client/game/version"],
|
&["/api/v1/client/game/version"],
|
||||||
&[
|
&[
|
||||||
("id", &meta.id),
|
("id", &meta.id),
|
||||||
("version", meta.version.as_ref().unwrap()),
|
("version", meta.version.as_ref().unwrap()),
|
||||||
],
|
],
|
||||||
)?;
|
|f| f.header("Authorization", header),
|
||||||
let response = client
|
)?
|
||||||
.get(response)
|
|
||||||
.header("Authorization", generate_authorization_header())
|
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let game_version: GameVersion = response.json()?;
|
let game_version: GameVersion = response.json()?;
|
||||||
@ -498,7 +488,6 @@ pub fn on_game_complete(
|
|||||||
.game_statuses
|
.game_statuses
|
||||||
.insert(meta.id.clone(), status.clone());
|
.insert(meta.id.clone(), status.clone());
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
|
|
||||||
app_handle
|
app_handle
|
||||||
.emit(
|
.emit(
|
||||||
&format!("update_game/{}", meta.id),
|
&format!("update_game/{}", meta.id),
|
||||||
@ -519,12 +508,6 @@ pub fn push_game_update(
|
|||||||
version: Option<GameVersion>,
|
version: Option<GameVersion>,
|
||||||
status: GameStatusWithTransient,
|
status: GameStatusWithTransient,
|
||||||
) {
|
) {
|
||||||
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
|
||||||
&status.0
|
|
||||||
&& version.is_none() {
|
|
||||||
panic!("pushed game for installed game that doesn't have version information");
|
|
||||||
}
|
|
||||||
|
|
||||||
app_handle
|
app_handle
|
||||||
.emit(
|
.emit(
|
||||||
&format!("update_game/{game_id}"),
|
&format!("update_game/{game_id}"),
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
#![deny(unused_must_use)]
|
|
||||||
#![feature(fn_traits)]
|
#![feature(fn_traits)]
|
||||||
#![feature(duration_constructors)]
|
#![feature(duration_constructors)]
|
||||||
#![feature(duration_millis_float)]
|
#![feature(duration_millis_float)]
|
||||||
#![feature(iterator_try_collect)]
|
|
||||||
#![deny(clippy::all)]
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
@ -41,7 +39,7 @@ use games::collections::commands::{
|
|||||||
fetch_collection, fetch_collections,
|
fetch_collection, fetch_collections,
|
||||||
};
|
};
|
||||||
use games::commands::{
|
use games::commands::{
|
||||||
fetch_game, fetch_game_status, fetch_game_version_options, fetch_library, uninstall_game,
|
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
|
||||||
};
|
};
|
||||||
use games::downloads::commands::download_game;
|
use games::downloads::commands::download_game;
|
||||||
use games::library::{Game, update_game_configuration};
|
use games::library::{Game, update_game_configuration};
|
||||||
@ -64,7 +62,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::panic::PanicHookInfo;
|
use std::panic::PanicHookInfo;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
@ -109,7 +108,13 @@ fn create_new_compat_info() -> Option<CompatInfo> {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
return None;
|
return None;
|
||||||
|
|
||||||
let has_umu_installed = *UMU_LAUNCHER_EXECUTABLE == PathBuf::new();
|
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 {
|
Some(CompatInfo {
|
||||||
umu_installed: has_umu_installed,
|
umu_installed: has_umu_installed,
|
||||||
})
|
})
|
||||||
@ -130,7 +135,7 @@ pub struct AppState<'a> {
|
|||||||
compat_info: Option<CompatInfo>,
|
compat_info: Option<CompatInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(handle: AppHandle) -> AppState<'static> {
|
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||||
let logfile = FileAppender::builder()
|
let logfile = FileAppender::builder()
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||||
@ -185,7 +190,7 @@ async fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
debug!("database is set up");
|
debug!("database is set up");
|
||||||
|
|
||||||
// TODO: Account for possible failure
|
// TODO: Account for possible failure
|
||||||
let (app_status, user) = auth::setup().await;
|
let (app_status, user) = auth::setup();
|
||||||
|
|
||||||
let db_handle = borrow_db_checked();
|
let db_handle = borrow_db_checked();
|
||||||
let mut missing_games = Vec::new();
|
let mut missing_games = Vec::new();
|
||||||
@ -312,7 +317,7 @@ pub fn run() {
|
|||||||
delete_download_dir,
|
delete_download_dir,
|
||||||
fetch_download_dir_stats,
|
fetch_download_dir_stats,
|
||||||
fetch_game_status,
|
fetch_game_status,
|
||||||
fetch_game_version_options,
|
fetch_game_verion_options,
|
||||||
update_game_configuration,
|
update_game_configuration,
|
||||||
// Collections
|
// Collections
|
||||||
fetch_collections,
|
fetch_collections,
|
||||||
@ -344,10 +349,8 @@ pub fn run() {
|
|||||||
))
|
))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
let state = setup(handle);
|
||||||
tauri::async_runtime::block_on(async move {
|
debug!("initialized drop client");
|
||||||
let state = setup(handle).await;
|
|
||||||
info!("initialized drop client");
|
|
||||||
app.manage(Mutex::new(state));
|
app.manage(Mutex::new(state));
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -377,27 +380,23 @@ pub fn run() {
|
|||||||
let binding = event.urls();
|
let binding = event.urls();
|
||||||
let url = binding.first().unwrap();
|
let url = binding.first().unwrap();
|
||||||
if url.host_str().unwrap() == "handshake" {
|
if url.host_str().unwrap() == "handshake" {
|
||||||
tauri::async_runtime::spawn(recieve_handshake(
|
recieve_handshake(handle.clone(), url.path().to_string());
|
||||||
handle.clone(),
|
|
||||||
url.path().to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let menu = Menu::with_items(
|
let menu = Menu::with_items(
|
||||||
app,
|
app,
|
||||||
&[
|
&[
|
||||||
&MenuItem::with_id(app, "open", "Open", true, None::<&str>).unwrap(),
|
&MenuItem::with_id(app, "open", "Open", true, None::<&str>)?,
|
||||||
&PredefinedMenuItem::separator(app).unwrap(),
|
&PredefinedMenuItem::separator(app)?,
|
||||||
/*
|
/*
|
||||||
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
&MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?,
|
||||||
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
&MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
*/
|
*/
|
||||||
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap(),
|
&MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?,
|
||||||
],
|
],
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
run_on_tray(|| {
|
run_on_tray(|| {
|
||||||
TrayIconBuilder::new()
|
TrayIconBuilder::new()
|
||||||
@ -435,7 +434,6 @@ pub fn run() {
|
|||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@ -444,21 +442,15 @@ pub fn run() {
|
|||||||
fetch_object(request, responder).await;
|
fetch_object(request, responder).await;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| {
|
.register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| {
|
||||||
tauri::async_runtime::block_on(async move {
|
let state: tauri::State<'_, Mutex<AppState>> = ctx.app_handle().state();
|
||||||
let state = ctx
|
|
||||||
.app_handle()
|
|
||||||
.state::<tauri::State<'_, Mutex<AppState>>>();
|
|
||||||
|
|
||||||
offline!(
|
offline!(
|
||||||
state,
|
state,
|
||||||
handle_server_proto,
|
handle_server_proto,
|
||||||
handle_server_proto_offline,
|
handle_server_proto_offline,
|
||||||
request,
|
request,
|
||||||
responder
|
responder
|
||||||
)
|
);
|
||||||
.await;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
|||||||
@ -1,10 +1,3 @@
|
|||||||
use std::{
|
|
||||||
ffi::OsStr,
|
|
||||||
path::PathBuf,
|
|
||||||
process::{Command, Stdio},
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -31,31 +24,7 @@ impl ProcessHandler for NativeGameLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static UMU_LAUNCHER_EXECUTABLE: LazyLock<PathBuf> = LazyLock::new(|| {
|
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
||||||
let x = get_umu_executable();
|
|
||||||
println!("{:?}", &x);
|
|
||||||
x
|
|
||||||
});
|
|
||||||
const UMU_BASE_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
|
||||||
const UMU_INSTALL_DIRS: [&str; 4] = ["/app/share", "/use/local/share", "/usr/share", "/opt"];
|
|
||||||
|
|
||||||
fn get_umu_executable() -> PathBuf {
|
|
||||||
if check_executable_exists(UMU_BASE_LAUNCHER_EXECUTABLE) {
|
|
||||||
return PathBuf::from(UMU_BASE_LAUNCHER_EXECUTABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
for dir in UMU_INSTALL_DIRS {
|
|
||||||
let p = PathBuf::from(dir).join(UMU_BASE_LAUNCHER_EXECUTABLE);
|
|
||||||
if check_executable_exists(&p) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PathBuf::new()
|
|
||||||
}
|
|
||||||
fn check_executable_exists<P: AsRef<OsStr>>(exec: P) -> bool {
|
|
||||||
let has_umu_installed = Command::new(exec).stdout(Stdio::null()).spawn();
|
|
||||||
has_umu_installed.is_ok()
|
|
||||||
}
|
|
||||||
pub struct UMULauncher;
|
pub struct UMULauncher;
|
||||||
impl ProcessHandler for UMULauncher {
|
impl ProcessHandler for UMULauncher {
|
||||||
fn create_launch_process(
|
fn create_launch_process(
|
||||||
@ -78,8 +47,8 @@ impl ProcessHandler for UMULauncher {
|
|||||||
None => game_version.game_id.clone(),
|
None => game_version.game_id.clone(),
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"GAMEID={game_id} {umu:?} \"{launch}\" {args}",
|
"GAMEID={game_id} {umu} \"{launch}\" {args}",
|
||||||
umu = &*UMU_LAUNCHER_EXECUTABLE,
|
umu = UMU_LAUNCHER_EXECUTABLE,
|
||||||
launch = launch_command,
|
launch = launch_command,
|
||||||
args = args.join(" ")
|
args = args.join(" ")
|
||||||
)
|
)
|
||||||
@ -111,10 +80,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
|||||||
game_version,
|
game_version,
|
||||||
current_dir,
|
current_dir,
|
||||||
);
|
);
|
||||||
let mut args_cmd = umu_string
|
let mut args_cmd = umu_string.split("umu-run").collect::<Vec<&str>>().into_iter();
|
||||||
.split("umu-run")
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.into_iter();
|
|
||||||
let args = args_cmd.next().unwrap().trim();
|
let args = args_cmd.next().unwrap().trim();
|
||||||
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
|
let cmd = format!("umu-run{}", args_cmd.next().unwrap());
|
||||||
|
|
||||||
|
|||||||
@ -172,23 +172,10 @@ impl ProcessManager<'_> {
|
|||||||
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
let _ = self.app_handle.emit("launch_external_error", &game_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is too many unwraps for me to be comfortable
|
|
||||||
let version_data = db_handle
|
|
||||||
.applications
|
|
||||||
.game_versions
|
|
||||||
.get(&game_id)
|
|
||||||
.unwrap()
|
|
||||||
.get(&meta.version.unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
|
let status = GameStatusManager::fetch_state(&game_id, &db_handle);
|
||||||
|
drop(db_handle);
|
||||||
|
|
||||||
push_game_update(
|
push_game_update(&self.app_handle, &game_id, None, status);
|
||||||
&self.app_handle,
|
|
||||||
&game_id,
|
|
||||||
Some(version_data.clone()),
|
|
||||||
status,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_process_handler(
|
fn fetch_process_handler(
|
||||||
@ -347,10 +334,11 @@ impl ProcessManager<'_> {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let mut command = Command::new("cmd");
|
let mut command = Command::new("start");
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
command.raw_arg(format!("/C \"{}\"", &launch_string));
|
command.raw_arg(format!("/min cmd /C \"{}\"", &launch_string));
|
||||||
|
|
||||||
info!("launching (in {install_dir}): {launch_string}",);
|
info!("launching (in {install_dir}): {launch_string}",);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use futures_lite::io;
|
use futures_lite::io;
|
||||||
use sysinfo::{Disk, DiskRefreshKind, Disks};
|
use sysinfo::{Disk, DiskRefreshKind, Disks};
|
||||||
@ -21,7 +21,7 @@ pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownlo
|
|||||||
return Ok(disk.available_space());
|
return Ok(disk.available_space());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(ApplicationDownloadError::IoError(Arc::new(io::Error::other(
|
Err(ApplicationDownloadError::IoError(io::Error::other(
|
||||||
"could not find disk of path",
|
"could not find disk of path",
|
||||||
))))
|
).kind()))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,12 +12,12 @@ use crate::{
|
|||||||
database::{
|
database::{
|
||||||
db::{borrow_db_checked, borrow_db_mut_checked},
|
db::{borrow_db_checked, borrow_db_mut_checked},
|
||||||
models::data::DatabaseAuth,
|
models::data::DatabaseAuth,
|
||||||
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::{requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User
|
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::utils::DROP_CLIENT_SYNC, AppState, AppStatus, User
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
cache::{cache_object, get_cached_object},
|
cache::{cache_object, get_cached_object},
|
||||||
requests::generate_url,
|
requests::make_request,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -61,10 +61,16 @@ pub fn generate_authorization_header() -> String {
|
|||||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||||
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
|
||||||
|
f.header("Authorization", header)
|
||||||
|
})?
|
||||||
|
.send()?;
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err: DropServerError = response.json().await?;
|
let err: DropServerError = response.json()?;
|
||||||
warn!("{err:?}");
|
warn!("{err:?}");
|
||||||
|
|
||||||
if err.status_message == "Nonce expired" {
|
if err.status_message == "Nonce expired" {
|
||||||
@ -74,13 +80,10 @@ pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
|||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response.json::<User>().map_err(std::convert::Into::into)
|
||||||
.json::<User>()
|
|
||||||
.await
|
|
||||||
.map_err(std::convert::Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||||
let path_chunks: Vec<&str> = path.split('/').collect();
|
let path_chunks: Vec<&str> = path.split('/').collect();
|
||||||
if path_chunks.len() != 3 {
|
if path_chunks.len() != 3 {
|
||||||
app.emit("auth/failed", ()).unwrap();
|
app.emit("auth/failed", ()).unwrap();
|
||||||
@ -102,13 +105,13 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
};
|
};
|
||||||
|
|
||||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
let response = client.post(endpoint).json(&body).send().await?;
|
let response = client.post(endpoint).json(&body).send()?;
|
||||||
debug!("handshake responsded with {}", response.status().as_u16());
|
debug!("handshake responsded with {}", response.status().as_u16());
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
return Err(RemoteAccessError::InvalidResponse(response.json()?));
|
||||||
}
|
}
|
||||||
let response_struct: HandshakeResponse = response.json().await?;
|
let response_struct: HandshakeResponse = response.json()?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut handle = borrow_db_mut_checked();
|
let mut handle = borrow_db_mut_checked();
|
||||||
@ -126,10 +129,9 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
||||||
.header("Authorization", header)
|
.header("Authorization", header)
|
||||||
.send()
|
.send()
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
token.text().await.unwrap()
|
token.text().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked();
|
let mut handle = borrow_db_mut_checked();
|
||||||
@ -139,11 +141,11 @@ async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||||
// Tell the app we're processing
|
// Tell the app we're processing
|
||||||
app.emit("auth/processing", ()).unwrap();
|
app.emit("auth/processing", ()).unwrap();
|
||||||
|
|
||||||
let handshake_result = recieve_handshake_logic(&app, path).await;
|
let handshake_result = recieve_handshake_logic(&app, path);
|
||||||
if let Err(e) = handshake_result {
|
if let Err(e) = handshake_result {
|
||||||
warn!("error with authentication: {e}");
|
warn!("error with authentication: {e}");
|
||||||
app.emit("auth/failed", e.to_string()).unwrap();
|
app.emit("auth/failed", e.to_string()).unwrap();
|
||||||
@ -151,11 +153,10 @@ pub async fn recieve_handshake(app: AppHandle, path: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let app_state = app.state::<Mutex<AppState>>();
|
let app_state = app.state::<Mutex<AppState>>();
|
||||||
|
|
||||||
let (app_status, user) = setup().await;
|
|
||||||
|
|
||||||
let mut state_lock = app_state.lock().unwrap();
|
let mut state_lock = app_state.lock().unwrap();
|
||||||
|
|
||||||
|
let (app_status, user) = setup();
|
||||||
|
|
||||||
state_lock.status = app_status;
|
state_lock.status = app_status;
|
||||||
state_lock.user = user;
|
state_lock.user = user;
|
||||||
|
|
||||||
@ -198,14 +199,13 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setup() -> (AppStatus, Option<User>) {
|
pub fn setup() -> (AppStatus, Option<User>) {
|
||||||
let auth = {
|
|
||||||
let data = borrow_db_checked();
|
let data = borrow_db_checked();
|
||||||
data.auth.clone()
|
let auth = data.auth.clone();
|
||||||
};
|
drop(data);
|
||||||
|
|
||||||
if auth.is_some() {
|
if auth.is_some() {
|
||||||
let user_result = match fetch_user().await {
|
let user_result = match fetch_user() {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(RemoteAccessError::FetchError(_)) => {
|
Err(RemoteAccessError::FetchError(_)) => {
|
||||||
let user = get_cached_object::<User>("user").unwrap();
|
let user = get_cached_object::<User>("user").unwrap();
|
||||||
|
|||||||
@ -16,11 +16,10 @@ use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder}
|
|||||||
macro_rules! offline {
|
macro_rules! offline {
|
||||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||||
|
|
||||||
async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
||||||
$func2( $( $arg ), *).await
|
$func2( $( $arg ), *)
|
||||||
} else {
|
} else {
|
||||||
$func1( $( $arg ), *).await
|
$func1( $( $arg ), *)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,14 +8,11 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, AppStatus,
|
database::db::{borrow_db_checked, borrow_db_mut_checked}, error::remote_access_error::RemoteAccessError, remote::{
|
||||||
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
remote::{
|
|
||||||
auth::generate_authorization_header,
|
auth::generate_authorization_header,
|
||||||
requests::generate_url,
|
requests::make_request,
|
||||||
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
|
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
|
||||||
},
|
}, AppState, AppStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -48,10 +45,9 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
||||||
let _drop_url = gen_drop_url(path.clone())?;
|
let _drop_url = gen_drop_url(path.clone())?;
|
||||||
let req = generate_url(&[&path], &[])?;
|
let req = make_request(&DROP_CLIENT_SYNC, &[&path], &[], |r| {
|
||||||
let req = DROP_CLIENT_SYNC
|
r.header("Authorization", generate_authorization_header())
|
||||||
.get(req)
|
})?
|
||||||
.header("Authorization", generate_authorization_header())
|
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
match req {
|
match req {
|
||||||
@ -87,15 +83,13 @@ pub fn sign_out(app: AppHandle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> {
|
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
let (app_status, user) = setup().await;
|
let (app_status, user) = setup();
|
||||||
|
|
||||||
let mut guard = state.lock().unwrap();
|
let mut guard = state.lock().unwrap();
|
||||||
guard.status = app_status;
|
guard.status = app_status;
|
||||||
guard.user = user;
|
guard.user = user;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -130,7 +124,7 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
|||||||
let code = auth_initiate_logic("code".to_string())?;
|
let code = auth_initiate_logic("code".to_string())?;
|
||||||
let header_code = code.clone();
|
let header_code = code.clone();
|
||||||
|
|
||||||
println!("using code: {code} to sign in");
|
println!("using code: {} to sign in", code);
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let load = async || -> Result<(), RemoteAccessError> {
|
let load = async || -> Result<(), RemoteAccessError> {
|
||||||
@ -151,7 +145,9 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
|||||||
match response.response_type.as_str() {
|
match response.response_type.as_str() {
|
||||||
"token" => {
|
"token" => {
|
||||||
let recieve_app = app.clone();
|
let recieve_app = app.clone();
|
||||||
manual_recieve_handshake(recieve_app, response.value).await.unwrap();
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
manual_recieve_handshake(recieve_app, response.value);
|
||||||
|
});
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
||||||
@ -175,8 +171,6 @@ pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), ()> {
|
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||||
recieve_handshake(app, format!("handshake/{token}")).await;
|
recieve_handshake(app, format!("handshake/{token}"));
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
use url::Url;
|
use reqwest::blocking::{Client, RequestBuilder};
|
||||||
|
|
||||||
use crate::{
|
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
|
||||||
DB,
|
|
||||||
database::db::DatabaseImpls,
|
|
||||||
error::remote_access_error::RemoteAccessError,
|
|
||||||
remote::{auth::generate_authorization_header, utils::DROP_CLIENT_ASYNC},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn generate_url<T: AsRef<str>>(
|
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
|
||||||
|
client: &Client,
|
||||||
path_components: &[T],
|
path_components: &[T],
|
||||||
query: &[(T, T)],
|
query: &[(T, T)],
|
||||||
) -> Result<Url, RemoteAccessError> {
|
f: F,
|
||||||
|
) -> Result<RequestBuilder, RemoteAccessError> {
|
||||||
let mut base_url = DB.fetch_base_url();
|
let mut base_url = DB.fetch_base_url();
|
||||||
for endpoint in path_components {
|
for endpoint in path_components {
|
||||||
base_url = base_url.join(endpoint.as_ref())?;
|
base_url = base_url.join(endpoint.as_ref())?;
|
||||||
@ -21,13 +18,6 @@ pub fn generate_url<T: AsRef<str>>(
|
|||||||
queries.append_pair(param.as_ref(), val.as_ref());
|
queries.append_pair(param.as_ref(), val.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(base_url)
|
let response = client.get(base_url);
|
||||||
}
|
Ok(f(response))
|
||||||
|
|
||||||
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, reqwest::Error> {
|
|
||||||
DROP_CLIENT_ASYNC
|
|
||||||
.get(url)
|
|
||||||
.header("Authorization", generate_authorization_header())
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use tauri::UriSchemeResponder;
|
|||||||
|
|
||||||
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC};
|
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()
|
let four_oh_four = Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.body(Vec::new())
|
.body(Vec::new())
|
||||||
@ -13,7 +13,7 @@ pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder:
|
|||||||
responder.respond(four_oh_four);
|
responder.respond(four_oh_four);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
let db_handle = borrow_db_checked();
|
let db_handle = borrow_db_checked();
|
||||||
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
let web_token = match &db_handle.auth.as_ref().unwrap().web_token {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||||
"productName": "Drop Desktop Client",
|
"productName": "Drop Desktop Client",
|
||||||
"version": "0.3.2",
|
"version": "0.3.2-dl",
|
||||||
"identifier": "dev.drop.client",
|
"identifier": "dev.drop.client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn --cwd main dev --port 1432",
|
"beforeDevCommand": "yarn --cwd main dev --port 1432",
|
||||||
|
|||||||
Reference in New Issue
Block a user