mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-15 01:01:25 +10:00
Compare commits
2 Commits
importexpo
...
ea6fa551a2
| Author | SHA1 | Date | |
|---|---|---|---|
| ea6fa551a2 | |||
| be4fc2d37a |
@ -21,13 +21,6 @@ async function spawn(exec, opts) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedLibs = ["drop-base/package.json"];
|
|
||||||
|
|
||||||
for (const lib of expectedLibs) {
|
|
||||||
const path = `./libs/${lib}`;
|
|
||||||
if (!fs.existsSync(path)) throw `Missing "${expectedLibs}". Run "git submodule update --init --recursive"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = fs.readdirSync(".").filter((view) => {
|
const views = fs.readdirSync(".").filter((view) => {
|
||||||
const expectedPath = `./${view}/package.json`;
|
const expectedPath = `./${view}/package.json`;
|
||||||
return fs.existsSync(expectedPath);
|
return fs.existsSync(expectedPath);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLoadingIndicator color="#2563eb" />
|
<LoadingIndicator />
|
||||||
<NuxtLayout class="select-none w-screen h-screen">
|
<NuxtLayout class="select-none w-screen h-screen">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<ModalStack />
|
<ModalStack />
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
<div class="h-0.5 rounded-full w-full bg-zinc-800" />
|
||||||
<div class="flex flex-col mb-1">
|
<div class="flex flex-col mb-1">
|
||||||
<MenuItem v-if="state.user.admin" v-slot="{ active }">
|
<MenuItem v-slot="{ active }">
|
||||||
<a
|
<a
|
||||||
:href="adminUrl"
|
:href="adminUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@ -1,118 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div class="mb-3 inline-flex gap-x-2">
|
<div class="mb-3 inline-flex gap-x-2">
|
||||||
<div
|
<div class="relative transition-transform duration-300 hover:scale-105 active:scale-95">
|
||||||
class="relative transition-transform duration-300 hover:scale-105 active:scale-95"
|
<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
|
|
||||||
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
|
<input type="text" v-model="searchQuery"
|
||||||
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
|
<button @click="() => calculateGames(true)"
|
||||||
@click="() => calculateGames(true, 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">
|
||||||
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">
|
||||||
<Disclosure
|
<NuxtLink v-for="(nav, navIndex) in filteredNavigation" :key="nav.id" :class="[
|
||||||
as="div"
|
'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',
|
||||||
v-for="(nav, navIndex) in filteredNavigation"
|
navIndex === currentNavigation
|
||||||
:key="nav.id"
|
|
||||||
class="first:pt-0 last:pb-0"
|
|
||||||
v-slot="{ open }"
|
|
||||||
:default-open="nav.deft"
|
|
||||||
>
|
|
||||||
<dt>
|
|
||||||
<DisclosureButton
|
|
||||||
class="flex w-full items-center justify-between text-left text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
<span class="text-sm font-semibold font-display">{{
|
|
||||||
nav.name
|
|
||||||
}}</span>
|
|
||||||
<span class="ml-6 flex h-7 items-center">
|
|
||||||
<PlusSmallIcon v-if="!open" class="size-6" aria-hidden="true" />
|
|
||||||
<MinusSmallIcon v-else class="size-6" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</DisclosureButton>
|
|
||||||
</dt>
|
|
||||||
<DisclosurePanel as="dd" class="mt-2 flex flex-col gap-y-1.5">
|
|
||||||
<NuxtLink
|
|
||||||
v-for="item in nav.items"
|
|
||||||
:key="nav.id"
|
|
||||||
:class="[
|
|
||||||
'transition-all duration-300 rounded-lg flex items-center px-1 py-1.5 hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-zinc-950/50',
|
|
||||||
currentNavigation == item.id
|
|
||||||
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
? 'bg-zinc-800 text-zinc-100 shadow-md shadow-zinc-950/20'
|
||||||
: item.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="item.route"
|
<div class="flex items-center w-full gap-x-3">
|
||||||
>
|
<div class="flex-none transition-transform duration-300 hover:-rotate-2">
|
||||||
<div class="flex items-center w-full gap-x-2">
|
<img class="size-8 object-cover bg-zinc-900 rounded-lg transition-all duration-300 shadow-sm"
|
||||||
<div
|
:src="icons[nav.id]" alt="" />
|
||||||
class="flex-none transition-transform duration-300 hover:-rotate-2"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="size-6 object-cover bg-zinc-900 rounded transition-all duration-300 shadow-sm"
|
|
||||||
:src="icons[item.id]"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center gap-x-2">
|
<div class="flex flex-col flex-1">
|
||||||
<p
|
<p class="truncate text-xs font-display leading-5 flex-1 font-semibold">
|
||||||
class="text-sm whitespace-nowrap font-display font-semibold"
|
{{ nav.label }}
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p class="text-xs font-medium" :class="[gameStatusTextStyle[games[nav.id].status.value.type]]">
|
||||||
class="truncate text-[10px] font-bold uppercase font-display"
|
{{ gameStatusText[games[nav.id].status.value.type] }}
|
||||||
:class="[
|
|
||||||
gameStatusTextStyle[games[item.id].status.value.type],
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ gameStatusText[games[item.id].status.value.type] }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
<div
|
<div v-if="loading" class="h-full grow flex p-8 justify-center text-zinc-100">
|
||||||
v-if="loading"
|
|
||||||
class="h-full grow flex p-8 justify-center text-zinc-100"
|
|
||||||
>
|
|
||||||
<div role="status">
|
<div role="status">
|
||||||
<svg
|
<svg aria-hidden="true" class="w-6 h-6 text-transparent animate-spin fill-zinc-600" viewBox="0 0 100 101"
|
||||||
aria-hidden="true"
|
fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
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
|
<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"
|
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"
|
fill="currentColor" />
|
||||||
/>
|
|
||||||
<path
|
<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"
|
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"
|
fill="currentFill" />
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
@ -121,20 +58,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
|
import { ArrowPathIcon, MagnifyingGlassIcon } from "@heroicons/vue/20/solid";
|
||||||
import {
|
|
||||||
ArrowPathIcon,
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
MinusSmallIcon,
|
|
||||||
PlusSmallIcon,
|
|
||||||
} from "@heroicons/vue/20/solid";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import {
|
import { GameStatusEnum, type Game, type GameStatus } from "~/types";
|
||||||
GameStatusEnum,
|
|
||||||
type Collection as Collection,
|
|
||||||
type Game,
|
|
||||||
type GameStatus,
|
|
||||||
} from "~/types";
|
|
||||||
import { TransitionGroup } from "vue";
|
import { TransitionGroup } from "vue";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
@ -144,7 +70,7 @@ const gameStatusTextStyle: { [key in GameStatusEnum]: string } = {
|
|||||||
[GameStatusEnum.Downloading]: "text-zinc-400",
|
[GameStatusEnum.Downloading]: "text-zinc-400",
|
||||||
[GameStatusEnum.Validating]: "text-blue-300",
|
[GameStatusEnum.Validating]: "text-blue-300",
|
||||||
[GameStatusEnum.Running]: "text-green-500",
|
[GameStatusEnum.Running]: "text-green-500",
|
||||||
[GameStatusEnum.Remote]: "text-zinc-700",
|
[GameStatusEnum.Remote]: "text-zinc-500",
|
||||||
[GameStatusEnum.Queued]: "text-zinc-400",
|
[GameStatusEnum.Queued]: "text-zinc-400",
|
||||||
[GameStatusEnum.Updating]: "text-zinc-400",
|
[GameStatusEnum.Updating]: "text-zinc-400",
|
||||||
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
[GameStatusEnum.Uninstalling]: "text-zinc-100",
|
||||||
@ -174,47 +100,26 @@ const games: {
|
|||||||
} = {};
|
} = {};
|
||||||
const icons: { [key: string]: string } = {};
|
const icons: { [key: string]: string } = {};
|
||||||
|
|
||||||
const collections: Ref<Collection[]> = ref([]);
|
const rawGames: Ref<Game[], Game[]> = ref([]);
|
||||||
|
|
||||||
async function calculateGames(clearAll = false, forceRefresh = false) {
|
async function calculateGames(clearAll = false) {
|
||||||
if (clearAll) {
|
if (clearAll) {
|
||||||
collections.value = [];
|
rawGames.value = [];
|
||||||
loading.value = true;
|
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<Game[]>("fetch_library", {
|
const newGames = await invoke<typeof rawGames.value>("fetch_library");
|
||||||
hardRefresh: forceRefresh,
|
for (const game of newGames) {
|
||||||
});
|
|
||||||
const otherCollections = await invoke<Collection[]>("fetch_collections", {
|
|
||||||
hardRefresh: forceRefresh,
|
|
||||||
});
|
|
||||||
const allGames = [
|
|
||||||
...newGames,
|
|
||||||
...otherCollections
|
|
||||||
.map((e) => e.entries)
|
|
||||||
.flat()
|
|
||||||
.map((e) => e.game),
|
|
||||||
].filter((v, i, a) => a.indexOf(v) === i);
|
|
||||||
|
|
||||||
for (const game of allGames) {
|
|
||||||
if (games[game.id]) continue;
|
if (games[game.id]) continue;
|
||||||
games[game.id] = await useGame(game.id);
|
games[game.id] = await useGame(game.id);
|
||||||
}
|
}
|
||||||
for (const game of allGames) {
|
for (const game of newGames) {
|
||||||
if (icons[game.id]) continue;
|
if (icons[game.id]) continue;
|
||||||
icons[game.id] = await useObject(game.mIconObjectId);
|
icons[game.id] = await useObject(game.mIconObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryCollection = {
|
|
||||||
id: "library",
|
|
||||||
name: "Library",
|
|
||||||
isDefault: true,
|
|
||||||
entries: newGames.map((e) => ({ gameId: e.id, game: e })),
|
|
||||||
} satisfies Collection;
|
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
collections.value = [libraryCollection, ...otherCollections];
|
rawGames.value = newGames;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to 300 ms for the library to load, otherwise
|
// Wait up to 300 ms for the library to load, otherwise
|
||||||
@ -223,19 +128,20 @@ await new Promise<void>((r) => {
|
|||||||
let hasResolved = false;
|
let hasResolved = false;
|
||||||
const resolveFunc = () => {
|
const resolveFunc = () => {
|
||||||
if (!hasResolved) r();
|
if (!hasResolved) r();
|
||||||
hasResolved = true;
|
hasResolved = true
|
||||||
};
|
|
||||||
|
}
|
||||||
calculateGames(true).then(resolveFunc);
|
calculateGames(true).then(resolveFunc);
|
||||||
setTimeout(resolveFunc, 300);
|
setTimeout(resolveFunc, 300);
|
||||||
});
|
})
|
||||||
|
|
||||||
const navigation = computed(() =>
|
const navigation = computed(() =>
|
||||||
collections.value.map((collection) => {
|
rawGames.value.map((game) => {
|
||||||
const items = collection.entries.map(({ game }) => {
|
|
||||||
const status = games[game.id].status;
|
const status = games[game.id].status;
|
||||||
|
|
||||||
const isInstalled = computed(
|
const isInstalled = computed(
|
||||||
() => status.value.type != GameStatusEnum.Remote
|
() =>
|
||||||
|
status.value.type != GameStatusEnum.Remote
|
||||||
);
|
);
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
@ -246,20 +152,12 @@ const navigation = computed(() =>
|
|||||||
id: game.id,
|
id: game.id,
|
||||||
};
|
};
|
||||||
return item;
|
return item;
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: collection.id,
|
|
||||||
name: collection.name,
|
|
||||||
deft: collection.isDefault,
|
|
||||||
items,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const currentNavigation = computed(() => {
|
const currentNavigation = computed(() => {
|
||||||
return route.path.slice("/library/".length);
|
return navigation.value.findIndex((e) => e.route == route.path)
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredNavigation = computed(() => {
|
const filteredNavigation = computed(() => {
|
||||||
@ -267,18 +165,15 @@ const filteredNavigation = computed(() => {
|
|||||||
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
return navigation.value.map((e, i) => ({ ...e, index: i }));
|
||||||
const query = searchQuery.value.toLowerCase();
|
const query = searchQuery.value.toLowerCase();
|
||||||
return navigation.value
|
return navigation.value
|
||||||
.map((c) => ({
|
.filter((nav) => nav.label.toLowerCase().includes(query))
|
||||||
...c,
|
.map((e, i) => ({ ...e, index: i }));
|
||||||
items: c.items.filter((nav) => nav.label.toLowerCase().includes(query)),
|
|
||||||
}))
|
|
||||||
.filter((e) => e.items.length > 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("update_library", async (event) => {
|
listen("update_library", async (event) => {
|
||||||
console.log("Updating library");
|
console.log("Updating library");
|
||||||
let oldNavigation = currentNavigation.value;
|
let oldNavigation = navigation.value[currentNavigation.value];
|
||||||
await calculateGames();
|
await calculateGames();
|
||||||
if (oldNavigation !== currentNavigation.value) {
|
if (oldNavigation.route !== navigation.value[currentNavigation.value].route) {
|
||||||
router.push("/library");
|
router.push("/library");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
7
main/components/LoadingIndicator.vue
Normal file
7
main/components/LoadingIndicator.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const loading = useLoadingIndicator();
|
||||||
|
|
||||||
|
watch(loading.isLoading, console.log);
|
||||||
|
</script>
|
||||||
@ -32,5 +32,3 @@ listen("update_stats", (event) => {
|
|||||||
const stats = useStatsState();
|
const stats = useStatsState();
|
||||||
stats.value = event.payload as StatsState;
|
stats.value = event.payload as StatsState;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);
|
|
||||||
@ -4,18 +4,18 @@
|
|||||||
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 font-display items-left justify-center pl-2"
|
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
|
||||||
>
|
>
|
||||||
<span class="font-bold text-zinc-100">{{ formatKilobytes(stats.speed) }}B/s</span>
|
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
|
||||||
<span v-if="stats.time > 0" class="text-xs text-zinc-400"
|
<span v-if="stats.time > 0" class="text-sm"
|
||||||
>{{ formatTime(stats.time) }} left</span
|
>{{ formatTime(stats.time) }} left</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-0 h-full flex flex-row items-end justify-end space-x-[1px]">
|
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
|
||||||
<div
|
<div
|
||||||
v-for="bar in speedHistory"
|
v-for="bar in speedHistory"
|
||||||
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
:style="{ height: `${(bar / speedMax) * 100}%` }"
|
||||||
class="w-[3px] bg-blue-600 rounded-t-full"
|
class="w-[8px] bg-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -62,9 +62,9 @@
|
|||||||
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
|
||||||
><span class="text-zinc-300">{{
|
><span class="text-zinc-300">{{
|
||||||
formatKilobytes(element.current / 1000)
|
formatKilobytes(element.current / 1000)
|
||||||
}}B</span>
|
}}</span>
|
||||||
/
|
/
|
||||||
<span class="">{{ formatKilobytes(element.max / 1000) }}B</span
|
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
|
||||||
><ServerIcon class="size-5"
|
><ServerIcon class="size-5"
|
||||||
/></span>
|
/></span>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +91,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
|
||||||
|
|
||||||
// const actionNames = {
|
// const actionNames = {
|
||||||
// [GameStatusEnum.Downloading]: "downloading",
|
// [GameStatusEnum.Downloading]: "downloading",
|
||||||
@ -105,12 +105,12 @@ window.addEventListener("resize", (event) => {
|
|||||||
|
|
||||||
const queue = useQueueState();
|
const queue = useQueueState();
|
||||||
const stats = useStatsState();
|
const stats = useStatsState();
|
||||||
const speedHistory = useDownloadHistory();
|
const speedHistory = useState<Array<number>>(() => []);
|
||||||
const speedHistoryMax = computed(() => windowWidth.value / 4);
|
const speedHistoryMax = computed(() => windowWidth.value / 8);
|
||||||
const speedMax = computed(
|
const speedMax = computed(
|
||||||
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.1
|
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
|
||||||
);
|
);
|
||||||
const previousGameId = useState<string | undefined>('previous_game');
|
const previousGameId = ref<string | undefined>();
|
||||||
|
|
||||||
const games: Ref<{
|
const games: Ref<{
|
||||||
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
|
||||||
@ -122,15 +122,14 @@ function resetHistoryGraph() {
|
|||||||
}
|
}
|
||||||
function checkReset(v: QueueState) {
|
function checkReset(v: QueueState) {
|
||||||
const currentGame = v.queue.at(0)?.meta.id;
|
const currentGame = v.queue.at(0)?.meta.id;
|
||||||
// If we don't have a game
|
|
||||||
if (!currentGame) return;
|
|
||||||
|
|
||||||
// If we're finished
|
// If we're finished
|
||||||
if (!currentGame && previousGameId.value) {
|
if (!currentGame && previousGameId.value) {
|
||||||
previousGameId.value = undefined;
|
previousGameId.value = undefined;
|
||||||
resetHistoryGraph();
|
resetHistoryGraph();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If we don't have a game
|
||||||
|
if (!currentGame) return;
|
||||||
// If we started a new download
|
// If we started a new download
|
||||||
if (currentGame && !previousGameId.value) {
|
if (currentGame && !previousGameId.value) {
|
||||||
previousGameId.value = currentGame;
|
previousGameId.value = currentGame;
|
||||||
@ -150,10 +149,9 @@ watch(queue, (v) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(stats, (v) => {
|
watch(stats, (v) => {
|
||||||
if(v.speed == 0) return;
|
|
||||||
const newLength = speedHistory.value.push(v.speed);
|
const newLength = speedHistory.value.push(v.speed);
|
||||||
if (newLength > speedHistoryMax.value) {
|
if (newLength > speedHistoryMax.value) {
|
||||||
speedHistory.value.splice(0, newLength - speedHistoryMax.value);
|
speedHistory.value.splice(0, 1);
|
||||||
}
|
}
|
||||||
checkReset(queue.value);
|
checkReset(queue.value);
|
||||||
});
|
});
|
||||||
@ -185,7 +183,7 @@ async function cancelGame(meta: DownloadableMetadata) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatKilobytes(bytes: number): string {
|
function formatKilobytes(bytes: number): string {
|
||||||
const units = ["K", "M", "G", "T", "P"];
|
const units = ["KB", "MB", "GB", "TB", "PB"];
|
||||||
let value = bytes;
|
let value = bytes;
|
||||||
let unitIndex = 0;
|
let unitIndex = 0;
|
||||||
const scalar = 1000;
|
const scalar = 1000;
|
||||||
|
|||||||
@ -37,13 +37,6 @@ export type Game = {
|
|||||||
mImageCarouselObjectIds: string[];
|
mImageCarouselObjectIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Collection = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
isDefault: boolean;
|
|
||||||
entries: Array<{ gameId: string; game: Game }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GameVersion = {
|
export type GameVersion = {
|
||||||
launchCommandTemplate: string;
|
launchCommandTemplate: string;
|
||||||
};
|
};
|
||||||
|
|||||||
1465
src-tauri/Cargo.lock
generated
1465
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,101 +1,118 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
# authors = ["Drop OSS"]
|
|
||||||
edition = "2024"
|
|
||||||
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"]
|
||||||
[workspace]
|
edition = "2024"
|
||||||
resolver = "3"
|
|
||||||
members = ["drop-consts",
|
|
||||||
"drop-database",
|
|
||||||
"drop-downloads",
|
|
||||||
"drop-errors", "drop-library",
|
|
||||||
"drop-native-library",
|
|
||||||
"drop-process",
|
|
||||||
"drop-remote",
|
|
||||||
]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||||
|
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib", "staticlib"]
|
|
||||||
# The `_lib` suffix may seem redundant but it is necessary
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
name = "drop_app_lib"
|
name = "drop_app_lib"
|
||||||
# rustflags = ["-C", "target-feature=+aes,+sse2"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||||
|
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.0.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
boxcar = "0.2.7"
|
|
||||||
dirs = "6.0.0"
|
|
||||||
drop-database = { path = "./drop-database" }
|
|
||||||
drop-downloads = { path = "./drop-downloads" }
|
|
||||||
drop-errors = { path = "./drop-errors" }
|
|
||||||
drop-native-library = { path = "./drop-native-library" }
|
|
||||||
drop-process = { path = "./drop-process" }
|
|
||||||
drop-remote = { path = "./drop-remote" }
|
|
||||||
futures-lite = "2.6.0"
|
|
||||||
hex = "0.4.3"
|
|
||||||
http = "1.1.0"
|
|
||||||
known-folders = "1.2.0"
|
|
||||||
log = "0.4.22"
|
|
||||||
md5 = "0.7.0"
|
|
||||||
rayon = "1.10.0"
|
|
||||||
regex = "1.11.1"
|
|
||||||
reqwest-websocket = "0.5.0"
|
|
||||||
serde_json = "1"
|
|
||||||
tar = "0.4.44"
|
|
||||||
tauri = { version = "2.7.0", features = ["protocol-asset", "tray-icon"] }
|
|
||||||
tauri-plugin-autostart = "2.0.0"
|
|
||||||
tauri-plugin-deep-link = "2"
|
|
||||||
tauri-plugin-dialog = "2"
|
|
||||||
tauri-plugin-opener = "2.4.0"
|
|
||||||
tauri-plugin-os = "2"
|
|
||||||
tauri-plugin-shell = "2.2.1"
|
tauri-plugin-shell = "2.2.1"
|
||||||
tempfile = "3.19.1"
|
serde_json = "1"
|
||||||
url = "2.5.2"
|
rayon = "1.10.0"
|
||||||
webbrowser = "1.0.2"
|
webbrowser = "1.0.2"
|
||||||
whoami = "1.6.0"
|
url = "2.5.2"
|
||||||
|
tauri-plugin-deep-link = "2"
|
||||||
|
log = "0.4.22"
|
||||||
|
hex = "0.4.3"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
http = "1.1.0"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
md5 = "0.7.0"
|
||||||
|
chrono = "0.4.38"
|
||||||
|
tauri-plugin-os = "2"
|
||||||
|
boxcar = "0.2.7"
|
||||||
|
umu-wrapper-lib = "0.1.0"
|
||||||
|
tauri-plugin-autostart = "2.0.0"
|
||||||
|
shared_child = "1.0.1"
|
||||||
|
serde_with = "3.12.0"
|
||||||
|
slice-deque = "0.3.0"
|
||||||
|
throttle_my_fn = "0.2.6"
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
atomic-instant-full = "0.1.0"
|
||||||
|
cacache = "13.1.0"
|
||||||
|
http-serde = "2.1.1"
|
||||||
|
reqwest-middleware = "0.4.0"
|
||||||
|
reqwest-middleware-cache = "0.1.1"
|
||||||
|
deranged = "=0.4.0"
|
||||||
|
droplet-rs = "0.7.3"
|
||||||
|
gethostname = "1.0.1"
|
||||||
zstd = "0.13.3"
|
zstd = "0.13.3"
|
||||||
|
tar = "0.4.44"
|
||||||
|
rand = "0.9.1"
|
||||||
|
regex = "1.11.1"
|
||||||
|
tempfile = "3.19.1"
|
||||||
|
schemars = "0.8.22"
|
||||||
|
sha1 = "0.10.6"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
whoami = "1.6.0"
|
||||||
|
filetime = "0.2.25"
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
known-folders = "1.2.0"
|
||||||
|
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
||||||
|
tauri-plugin-opener = "2.4.0"
|
||||||
|
bitcode = "0.6.6"
|
||||||
|
reqwest-websocket = "0.5.0"
|
||||||
|
futures-lite = "2.6.0"
|
||||||
|
page_size = "0.6.0"
|
||||||
|
sysinfo = "0.36.1"
|
||||||
|
humansize = "2.1.3"
|
||||||
|
# tailscale = { path = "./tailscale" }
|
||||||
|
|
||||||
|
[dependencies.dynfmt]
|
||||||
|
version = "0.1.5"
|
||||||
|
features = ["curly"]
|
||||||
|
|
||||||
|
[dependencies.tauri]
|
||||||
|
version = "2.7.0"
|
||||||
|
features = ["protocol-asset", "tray-icon"]
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
version = "1.40.0"
|
||||||
|
features = ["rt", "tokio-macros", "signal"]
|
||||||
|
|
||||||
[dependencies.log4rs]
|
[dependencies.log4rs]
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
features = ["console_appender", "file_appender"]
|
features = ["console_appender", "file_appender"]
|
||||||
|
|
||||||
[dependencies.reqwest]
|
|
||||||
version = "0.12.22"
|
|
||||||
default-features = false
|
|
||||||
features = [
|
|
||||||
"blocking",
|
|
||||||
"http2",
|
|
||||||
"json",
|
|
||||||
"native-tls-alpn",
|
|
||||||
"rustls-tls",
|
|
||||||
"rustls-tls-native-roots",
|
|
||||||
"stream",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies.rustix]
|
[dependencies.rustix]
|
||||||
version = "0.38.37"
|
version = "0.38.37"
|
||||||
features = ["fs"]
|
features = ["fs"]
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
version = "1.10.0"
|
||||||
|
features = ["v4", "fast-rng", "macro-diagnostics"]
|
||||||
|
|
||||||
|
[dependencies.rustbreak]
|
||||||
|
version = "2"
|
||||||
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "0.12.22"
|
||||||
|
default-features = false
|
||||||
|
features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
features = ["derive", "rc"]
|
features = ["derive", "rc"]
|
||||||
|
|
||||||
[dependencies.uuid]
|
|
||||||
version = "1.10.0"
|
|
||||||
features = ["fast-rng", "macro-diagnostics", "v4"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "2.0.0", features = [] }
|
|
||||||
|
|
||||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
|
||||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
panic = 'abort'
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-consts"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dirs = "6.0.0"
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
use std::{
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, LazyLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
static DATA_ROOT_PREFIX: &'static str = "drop";
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
static DATA_ROOT_PREFIX: &str = "drop-debug";
|
|
||||||
|
|
||||||
pub static DATA_ROOT_DIR: LazyLock<&'static PathBuf> =
|
|
||||||
LazyLock::new(|| Box::leak(Box::new(dirs::data_dir().unwrap().join(DATA_ROOT_PREFIX))));
|
|
||||||
|
|
||||||
pub static CACHE_DIR: LazyLock<&'static PathBuf> =
|
|
||||||
LazyLock::new(|| Box::leak(Box::new(DATA_ROOT_DIR.join("cache"))));
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-database"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bitcode = "0.6.7"
|
|
||||||
chrono = "0.4.42"
|
|
||||||
drop-consts = { path = "../drop-consts" }
|
|
||||||
drop-library = { path = "../drop-library" }
|
|
||||||
drop-native-library = { path = "../drop-native-library" }
|
|
||||||
log = "0.4.28"
|
|
||||||
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
|
|
||||||
"rmp_serde_1_3",
|
|
||||||
] }
|
|
||||||
rustbreak = "2.0.0"
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
serde_with = "3.14.0"
|
|
||||||
url = "2.5.7"
|
|
||||||
whoami = "1.6.1"
|
|
||||||
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
use std::{mem::ManuallyDrop, sync::LazyLock};
|
|
||||||
|
|
||||||
use log::error;
|
|
||||||
|
|
||||||
use crate::db::{DBRead, DBWrite, DatabaseImpls, DatabaseInterface};
|
|
||||||
|
|
||||||
pub mod db;
|
|
||||||
pub mod debug;
|
|
||||||
pub mod models;
|
|
||||||
pub mod process;
|
|
||||||
pub mod runtime_models;
|
|
||||||
pub mod drop_data;
|
|
||||||
|
|
||||||
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
|
||||||
|
|
||||||
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
|
||||||
match DB.borrow_data() {
|
|
||||||
Ok(data) => DBRead(data),
|
|
||||||
Err(e) => {
|
|
||||||
error!("database borrow failed with error {e}");
|
|
||||||
panic!("database borrow failed with error {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
|
||||||
match DB.borrow_data_mut() {
|
|
||||||
Ok(data) => DBWrite(ManuallyDrop::new(data)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("database borrow mut failed with error {e}");
|
|
||||||
panic!("database borrow mut failed with error {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
|
||||||
pub enum Platform {
|
|
||||||
Windows,
|
|
||||||
Linux,
|
|
||||||
MacOs,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Platform {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub const HOST: Platform = Self::Windows;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub const HOST: Platform = Self::MacOs;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub const HOST: Platform = Self::Linux;
|
|
||||||
|
|
||||||
pub fn is_case_sensitive(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Windows | Self::MacOs => false,
|
|
||||||
Self::Linux => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for Platform {
|
|
||||||
fn from(value: &str) -> Self {
|
|
||||||
match value.to_lowercase().trim() {
|
|
||||||
"windows" => Self::Windows,
|
|
||||||
"linux" => Self::Linux,
|
|
||||||
"mac" | "macos" => Self::MacOs,
|
|
||||||
_ => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<whoami::Platform> for Platform {
|
|
||||||
fn from(value: whoami::Platform) -> Self {
|
|
||||||
match value {
|
|
||||||
whoami::Platform::Windows => Platform::Windows,
|
|
||||||
whoami::Platform::Linux => Platform::Linux,
|
|
||||||
whoami::Platform::MacOS => Platform::MacOs,
|
|
||||||
_ => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
use bitcode::{Decode, Encode};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Game {
|
|
||||||
pub id: String,
|
|
||||||
m_name: String,
|
|
||||||
m_short_description: String,
|
|
||||||
m_description: String,
|
|
||||||
// mDevelopers
|
|
||||||
// mPublishers
|
|
||||||
m_icon_object_id: String,
|
|
||||||
m_banner_object_id: String,
|
|
||||||
m_cover_object_id: String,
|
|
||||||
m_image_library_object_ids: Vec<String>,
|
|
||||||
m_image_carousel_object_ids: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct User {
|
|
||||||
id: String,
|
|
||||||
username: String,
|
|
||||||
admin: bool,
|
|
||||||
display_name: String,
|
|
||||||
profile_picture_object_id: String,
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-downloads"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
atomic-instant-full = "0.1.0"
|
|
||||||
drop-database = { path = "../drop-database" }
|
|
||||||
drop-errors = { path = "../drop-errors" }
|
|
||||||
# can't depend, cycle
|
|
||||||
# drop-native-library = { path = "../drop-native-library" }
|
|
||||||
log = "0.4.22"
|
|
||||||
parking_lot = "0.12.4"
|
|
||||||
serde = "1.0.219"
|
|
||||||
tauri = { version = "2.7.0" }
|
|
||||||
throttle_my_fn = "0.2.6"
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
use drop_database::models::data::DownloadableMetadata;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::download_manager_frontend::DownloadStatus;
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub struct QueueUpdateEventQueueData {
|
|
||||||
pub meta: DownloadableMetadata,
|
|
||||||
pub status: DownloadStatus,
|
|
||||||
pub progress: f64,
|
|
||||||
pub current: usize,
|
|
||||||
pub max: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub struct QueueUpdateEvent {
|
|
||||||
pub queue: Vec<QueueUpdateEventQueueData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub struct StatsUpdateEvent {
|
|
||||||
pub speed: usize,
|
|
||||||
pub time: usize,
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#![feature(duration_millis_float)]
|
|
||||||
|
|
||||||
pub mod download_manager_builder;
|
|
||||||
pub mod download_manager_frontend;
|
|
||||||
pub mod downloadable;
|
|
||||||
pub mod events;
|
|
||||||
pub mod util;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-errors"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
http = "1.3.1"
|
|
||||||
humansize = "2.1.3"
|
|
||||||
reqwest = "0.12.23"
|
|
||||||
reqwest-websocket = "0.5.1"
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
serde_with = "3.14.0"
|
|
||||||
tauri-plugin-opener = "2.5.0"
|
|
||||||
url = "2.5.7"
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-library"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
drop-errors = { path = "../drop-errors" }
|
|
||||||
http = "*"
|
|
||||||
reqwest = { version = "*", default-features = false }
|
|
||||||
serde = { version = "*", default-features = false, features = ["derive"] }
|
|
||||||
tauri = "*"
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
pub enum DropLibraryError {
|
|
||||||
NetworkError(reqwest::Error),
|
|
||||||
ServerError(drop_errors::drop_server_error::ServerError),
|
|
||||||
Unconfigured,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for DropLibraryError {
|
|
||||||
fn from(value: reqwest::Error) -> Self {
|
|
||||||
DropLibraryError::NetworkError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
use crate::libraries::LibraryProviderIdentifier;
|
|
||||||
|
|
||||||
pub struct LibraryGamePreview {
|
|
||||||
pub library: LibraryProviderIdentifier,
|
|
||||||
pub internal_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub short_description: String,
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LibraryGame {
|
|
||||||
pub library: LibraryProviderIdentifier,
|
|
||||||
pub internal_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub short_description: String,
|
|
||||||
pub md_description: String,
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<LibraryGame> for LibraryGamePreview {
|
|
||||||
fn from(value: LibraryGame) -> Self {
|
|
||||||
LibraryGamePreview {
|
|
||||||
library: value.library,
|
|
||||||
internal_id: value.internal_id,
|
|
||||||
name: value.name,
|
|
||||||
short_description: value.short_description,
|
|
||||||
icon: value.icon,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod libraries;
|
|
||||||
pub mod game;
|
|
||||||
pub mod errors;
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
|
||||||
};
|
|
||||||
|
|
||||||
use http::Request;
|
|
||||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
|
||||||
use tauri::UriSchemeResponder;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
errors::DropLibraryError,
|
|
||||||
game::{LibraryGame, LibraryGamePreview},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LibraryProviderIdentifier {
|
|
||||||
internal_id: usize,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for LibraryProviderIdentifier {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.internal_id == other.internal_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for LibraryProviderIdentifier {}
|
|
||||||
|
|
||||||
impl Hash for LibraryProviderIdentifier {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.internal_id.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for LibraryProviderIdentifier {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LibraryProviderIdentifier {
|
|
||||||
pub fn str_hash(&self) -> String {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
self.hash(&mut hasher);
|
|
||||||
hasher.finish().to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LibraryFetchConfig {
|
|
||||||
pub hard_refresh: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DropLibraryProvider: Serialize + DeserializeOwned + Sized {
|
|
||||||
fn build(identifier: LibraryProviderIdentifier) -> Self;
|
|
||||||
fn id(&self) -> &LibraryProviderIdentifier;
|
|
||||||
fn load_object(
|
|
||||||
&self,
|
|
||||||
request: Request<Vec<u8>>,
|
|
||||||
responder: UriSchemeResponder,
|
|
||||||
) -> impl Future<Output = Result<(), DropLibraryError>> + Send;
|
|
||||||
|
|
||||||
fn fetch_library(
|
|
||||||
&self,
|
|
||||||
config: &LibraryFetchConfig,
|
|
||||||
) -> impl Future<Output = Result<Vec<LibraryGamePreview>, DropLibraryError>> + Send;
|
|
||||||
fn fetch_game(
|
|
||||||
&self,
|
|
||||||
config: &LibraryFetchConfig,
|
|
||||||
) -> impl Future<Output = Result<LibraryGame, DropLibraryError>> + Send;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn owns_game(&self, id: &LibraryProviderIdentifier) -> bool {
|
|
||||||
self.id().internal_id == id.internal_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-native-library"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bitcode = "*"
|
|
||||||
drop-errors = { path = "../drop-errors" }
|
|
||||||
drop-library = { path = "../drop-library" }
|
|
||||||
drop-remote = { path = "../drop-remote" }
|
|
||||||
log = "*"
|
|
||||||
serde = { version = "*", features = ["derive"] }
|
|
||||||
tauri = "*"
|
|
||||||
url = "*"
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
use drop_database::models::data::{ApplicationTransientStatus, GameDownloadStatus, GameVersion};
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone)]
|
|
||||||
pub struct GameUpdateEvent {
|
|
||||||
pub game_id: String,
|
|
||||||
pub status: (
|
|
||||||
Option<GameDownloadStatus>,
|
|
||||||
Option<ApplicationTransientStatus>,
|
|
||||||
),
|
|
||||||
pub version: Option<GameVersion>,
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
use drop_library::{
|
|
||||||
errors::DropLibraryError, game::{LibraryGame, LibraryGamePreview}, libraries::{DropLibraryProvider, LibraryFetchConfig, LibraryProviderIdentifier}
|
|
||||||
};
|
|
||||||
use drop_remote::{fetch_object::fetch_object, DropRemoteContext};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub struct DropNativeLibraryProvider {
|
|
||||||
identifier: LibraryProviderIdentifier,
|
|
||||||
context: Option<DropRemoteContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DropNativeLibraryProvider {
|
|
||||||
pub fn configure(&mut self, base_url: Url) {
|
|
||||||
self.context = Some(DropRemoteContext::new(base_url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DropLibraryProvider for DropNativeLibraryProvider {
|
|
||||||
fn build(identifier: LibraryProviderIdentifier) -> Self {
|
|
||||||
Self {
|
|
||||||
identifier,
|
|
||||||
context: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> &LibraryProviderIdentifier {
|
|
||||||
&self.identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_object(&self, request: tauri::http::Request<Vec<u8>>, responder: tauri::UriSchemeResponder) -> Result<(), DropLibraryError> {
|
|
||||||
let context = self.context.as_ref().ok_or(DropLibraryError::Unconfigured)?;
|
|
||||||
fetch_object(context, request, responder).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_library(
|
|
||||||
&self,
|
|
||||||
config: &LibraryFetchConfig
|
|
||||||
) -> Result<Vec<LibraryGamePreview>, DropLibraryError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_game(&self, config: &LibraryFetchConfig) -> Result<LibraryGame, DropLibraryError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
//pub mod collections;
|
|
||||||
//pub mod library;
|
|
||||||
//pub mod state;
|
|
||||||
//pub mod events;
|
|
||||||
pub mod impls;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-process"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
chrono = "0.4.42"
|
|
||||||
drop-database = { path = "../drop-database" }
|
|
||||||
drop-errors = { path = "../drop-errors" }
|
|
||||||
drop-native-library = { path = "../drop-native-library" }
|
|
||||||
dynfmt = { version = "0.1.5", features = ["curly"] }
|
|
||||||
log = "0.4.28"
|
|
||||||
page_size = "0.6.0"
|
|
||||||
shared_child = "1.1.1"
|
|
||||||
sysinfo = "0.37.0"
|
|
||||||
tauri = "2.8.5"
|
|
||||||
tauri-plugin-opener = "2.5.0"
|
|
||||||
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
mod format;
|
|
||||||
mod process_handlers;
|
|
||||||
pub mod process_manager;
|
|
||||||
pub mod utils;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "drop-remote"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bitcode = "0.6.7"
|
|
||||||
chrono = "0.4.42"
|
|
||||||
drop-consts = { path = "../drop-consts" }
|
|
||||||
drop-errors = { path = "../drop-errors" }
|
|
||||||
droplet-rs = "0.7.3"
|
|
||||||
gethostname = "1.0.2"
|
|
||||||
hex = "0.4.3"
|
|
||||||
http = "1.3.1"
|
|
||||||
log = "0.4.28"
|
|
||||||
md5 = "0.8.0"
|
|
||||||
reqwest = "0.12.23"
|
|
||||||
serde = { version = "1.0.220", features = ["derive"] }
|
|
||||||
tauri = "2.8.5"
|
|
||||||
url = "2.5.7"
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
use std::{collections::HashMap, env, sync::Mutex};
|
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
use drop_errors::{drop_server_error::ServerError, remote_access_error::RemoteAccessError};
|
|
||||||
use droplet_rs::ssl::sign_nonce;
|
|
||||||
use gethostname::gethostname;
|
|
||||||
use log::{debug, error, warn};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}, DropRemoteAuth, DropRemoteContext
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::requests::generate_url;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct CapabilityConfiguration {}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct InitiateRequestBody {
|
|
||||||
name: String,
|
|
||||||
platform: String,
|
|
||||||
capabilities: HashMap<String, CapabilityConfiguration>,
|
|
||||||
mode: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct HandshakeRequestBody {
|
|
||||||
client_id: String,
|
|
||||||
token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct HandshakeResponse {
|
|
||||||
private: String,
|
|
||||||
certificate: String,
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_authorization_header(context: &DropRemoteContext) -> String {
|
|
||||||
let auth = if let Some(auth) = &context.auth {
|
|
||||||
auth
|
|
||||||
} else {
|
|
||||||
return "".to_owned();
|
|
||||||
};
|
|
||||||
let nonce = Utc::now().timestamp_millis().to_string();
|
|
||||||
|
|
||||||
let signature = sign_nonce(auth.private.clone(), nonce.clone()).unwrap();
|
|
||||||
|
|
||||||
format!("Nonce {} {} {}", auth.client_id, nonce, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_user(context: &DropRemoteContext) -> Result<Vec<u8>, RemoteAccessError> {
|
|
||||||
let response =
|
|
||||||
make_authenticated_get(context, generate_url(context, &["/api/v1/client/user"], &[])?).await?;
|
|
||||||
if response.status() != 200 {
|
|
||||||
let err: ServerError = response.json().await?;
|
|
||||||
warn!("{err:?}");
|
|
||||||
|
|
||||||
if err.status_message == "Nonce expired" {
|
|
||||||
return Err(RemoteAccessError::OutOfSync);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(RemoteAccessError::InvalidResponse(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
.bytes()
|
|
||||||
.await
|
|
||||||
.map_err(std::convert::Into::into)
|
|
||||||
.map(|v| v.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn recieve_handshake_logic(
|
|
||||||
context: &mut DropRemoteContext,
|
|
||||||
path: String,
|
|
||||||
) -> Result<(), RemoteAccessError> {
|
|
||||||
let path_chunks: Vec<&str> = path.split('/').collect();
|
|
||||||
if path_chunks.len() != 3 {
|
|
||||||
// app.emit("auth/failed", ()).unwrap();
|
|
||||||
return Err(RemoteAccessError::HandshakeFailed(
|
|
||||||
"failed to parse token".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let client_id = path_chunks.get(1).unwrap();
|
|
||||||
let token = path_chunks.get(2).unwrap();
|
|
||||||
let body = HandshakeRequestBody {
|
|
||||||
client_id: (*client_id).to_string(),
|
|
||||||
token: (*token).to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let endpoint = generate_url(context, &["/api/v1/client/auth/handshake"], &[])?;
|
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
|
||||||
let response = client.post(endpoint).json(&body).send().await?;
|
|
||||||
debug!("handshake responsded with {}", response.status().as_u16());
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
|
||||||
}
|
|
||||||
let response_struct: HandshakeResponse = response.json().await?;
|
|
||||||
|
|
||||||
let web_token = {
|
|
||||||
let header = generate_authorization_header(context);
|
|
||||||
let token = client
|
|
||||||
.post(generate_url(context, &["/api/v1/client/user/webtoken"], &[])?)
|
|
||||||
.header("Authorization", header)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
token.text().await.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
context.auth = Some(DropRemoteAuth {
|
|
||||||
private: response_struct.private,
|
|
||||||
cert: response_struct.certificate,
|
|
||||||
client_id: response_struct.id,
|
|
||||||
web_token: web_token,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auth_initiate_logic(context: &DropRemoteContext, mode: String) -> Result<String, RemoteAccessError> {
|
|
||||||
let hostname = gethostname();
|
|
||||||
|
|
||||||
let endpoint = generate_url(context, &["/api/v1/client/auth/initiate"], &[])?;
|
|
||||||
let body = InitiateRequestBody {
|
|
||||||
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
|
|
||||||
platform: env::consts::OS.to_string(),
|
|
||||||
capabilities: HashMap::from([
|
|
||||||
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
|
||||||
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
|
||||||
]),
|
|
||||||
mode,
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = DROP_CLIENT_SYNC.clone();
|
|
||||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
|
||||||
|
|
||||||
if response.status() != 200 {
|
|
||||||
let data: ServerError = response.json()?;
|
|
||||||
error!("could not start handshake: {}", data.status_message);
|
|
||||||
|
|
||||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = response.text()?;
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod cache;
|
|
||||||
pub mod fetch_object;
|
|
||||||
pub mod requests;
|
|
||||||
pub mod utils;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
struct DropRemoteAuth {
|
|
||||||
private: String,
|
|
||||||
cert: String,
|
|
||||||
client_id: String,
|
|
||||||
web_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub struct DropRemoteContext {
|
|
||||||
base_url: Url,
|
|
||||||
auth: Option<DropRemoteAuth>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl DropRemoteContext {
|
|
||||||
pub fn new(base_url: Url) -> Self {
|
|
||||||
DropRemoteContext { base_url, auth: None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs::{self, File},
|
|
||||||
io::Read,
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use drop_consts::DATA_ROOT_DIR;
|
|
||||||
use log::{debug, info};
|
|
||||||
use reqwest::Certificate;
|
|
||||||
|
|
||||||
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
|
|
||||||
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
|
|
||||||
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
|
|
||||||
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
|
|
||||||
|
|
||||||
fn fetch_certificates() -> Vec<Certificate> {
|
|
||||||
let certificate_dir = DATA_ROOT_DIR.join("certificates");
|
|
||||||
|
|
||||||
let mut certs = Vec::new();
|
|
||||||
match fs::read_dir(certificate_dir) {
|
|
||||||
Ok(c) => {
|
|
||||||
for entry in c {
|
|
||||||
match entry {
|
|
||||||
Ok(c) => {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
File::open(c.path()).unwrap().read_to_end(&mut buf).unwrap();
|
|
||||||
|
|
||||||
for cert in Certificate::from_pem_bundle(&buf).unwrap() {
|
|
||||||
certs.push(cert);
|
|
||||||
}
|
|
||||||
info!(
|
|
||||||
"added {} certificate(s) from {}",
|
|
||||||
certs.len(),
|
|
||||||
c.file_name().into_string().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(_) => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!("not loading certificates due to error: {e}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
certs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_client_sync() -> reqwest::blocking::Client {
|
|
||||||
let mut client = reqwest::blocking::ClientBuilder::new();
|
|
||||||
|
|
||||||
for cert in DROP_CERT_BUNDLE.iter() {
|
|
||||||
client = client.add_root_certificate(cert.clone());
|
|
||||||
}
|
|
||||||
client.use_rustls_tls().build().unwrap()
|
|
||||||
}
|
|
||||||
pub fn get_client_async() -> reqwest::Client {
|
|
||||||
let mut client = reqwest::ClientBuilder::new();
|
|
||||||
|
|
||||||
for cert in DROP_CERT_BUNDLE.iter() {
|
|
||||||
client = client.add_root_certificate(cert.clone());
|
|
||||||
}
|
|
||||||
client.use_rustls_tls().build().unwrap()
|
|
||||||
}
|
|
||||||
pub fn get_client_ws() -> reqwest::Client {
|
|
||||||
let mut client = reqwest::ClientBuilder::new();
|
|
||||||
|
|
||||||
for cert in DROP_CERT_BUNDLE.iter() {
|
|
||||||
client = client.add_root_certificate(cert.clone());
|
|
||||||
}
|
|
||||||
client.use_rustls_tls().http1_only().build().unwrap()
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, runtime_models::User};
|
|
||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
|
||||||
use drop_remote::{auth::{fetch_user, recieve_handshake_logic}, cache::{cache_object, clear_cached_object, get_cached_object}};
|
|
||||||
use log::warn;
|
|
||||||
use tauri::{AppHandle, Emitter as _, Manager as _};
|
|
||||||
|
|
||||||
use crate::{AppState, AppStatus};
|
|
||||||
|
|
||||||
pub async fn setup() -> (AppStatus, Option<User>) {
|
|
||||||
let auth = {
|
|
||||||
let data = borrow_db_checked();
|
|
||||||
data.auth.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if auth.is_some() {
|
|
||||||
let user_result = match fetch_user().await {
|
|
||||||
Ok(data) => data,
|
|
||||||
Err(RemoteAccessError::FetchError(_)) => {
|
|
||||||
let user = get_cached_object::<User>("user").unwrap();
|
|
||||||
return (AppStatus::Offline, Some(user));
|
|
||||||
}
|
|
||||||
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
|
||||||
};
|
|
||||||
cache_object("user", &user_result).unwrap();
|
|
||||||
return (AppStatus::SignedIn, Some(user_result));
|
|
||||||
}
|
|
||||||
|
|
||||||
(AppStatus::SignedOut, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
|
||||||
// Tell the app we're processing
|
|
||||||
app.emit("auth/processing", ()).unwrap();
|
|
||||||
|
|
||||||
let handshake_result = recieve_handshake_logic(path).await;
|
|
||||||
if let Err(e) = handshake_result {
|
|
||||||
warn!("error with authentication: {e}");
|
|
||||||
app.emit("auth/failed", e.to_string()).unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_state = app.state::<Mutex<AppState>>();
|
|
||||||
|
|
||||||
let (app_status, user) = setup().await;
|
|
||||||
|
|
||||||
let mut state_lock = app_state.lock().unwrap();
|
|
||||||
|
|
||||||
state_lock.status = app_status;
|
|
||||||
state_lock.user = user;
|
|
||||||
|
|
||||||
let _ = clear_cached_object("collections");
|
|
||||||
let _ = clear_cached_object("library");
|
|
||||||
|
|
||||||
drop(state_lock);
|
|
||||||
|
|
||||||
app.emit("auth/finished", ()).unwrap();
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_autostart::ManagerExt;
|
use tauri_plugin_autostart::ManagerExt;
|
||||||
|
|||||||
@ -4,11 +4,11 @@ use tauri::AppHandle;
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState>>) {
|
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
cleanup_and_exit(&app, &state);
|
cleanup_and_exit(&app, &state);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState>>) {
|
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
|
||||||
debug!("cleaning up and exiting application");
|
debug!("cleaning up and exiting application");
|
||||||
let download_manager = state.lock().unwrap().download_manager.clone();
|
let download_manager = state.lock().unwrap().download_manager.clone();
|
||||||
match download_manager.ensure_terminated() {
|
match download_manager.ensure_terminated() {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use crate::AppState;
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_state(
|
pub fn fetch_state(
|
||||||
state: tauri::State<'_, std::sync::Mutex<AppState>>,
|
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let guard = state.lock().unwrap();
|
let guard = state.lock().unwrap();
|
||||||
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
|
||||||
|
|||||||
@ -4,12 +4,17 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, debug::SystemData, models::data::Settings};
|
|
||||||
use drop_errors::download_manager_error::DownloadManagerError;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::database::scan::scan_install_dirs;
|
use crate::{
|
||||||
|
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
db::{borrow_db_checked, DATA_ROOT_DIR},
|
||||||
|
debug::SystemData,
|
||||||
|
models::data::Settings,
|
||||||
|
};
|
||||||
|
|
||||||
// Will, in future, return disk/remaining size
|
// Will, in future, return disk/remaining size
|
||||||
// Just returns the directories that have been set up
|
// Just returns the directories that have been set up
|
||||||
|
|||||||
@ -7,15 +7,19 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use drop_consts::DATA_ROOT_DIR;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
|
use native_model::{Decode, Encode};
|
||||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::DB;
|
use crate::DB;
|
||||||
|
|
||||||
use super::models::data::Database;
|
use super::models::data::Database;
|
||||||
|
|
||||||
|
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
|
||||||
|
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join("drop")));
|
||||||
|
|
||||||
// Custom JSON serializer to support everything we need
|
// Custom JSON serializer to support everything we need
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct DropDatabaseSerializer;
|
pub struct DropDatabaseSerializer;
|
||||||
@ -24,15 +28,16 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
|||||||
for DropDatabaseSerializer
|
for DropDatabaseSerializer
|
||||||
{
|
{
|
||||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||||
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
native_model::rmp_serde_1_3::RmpSerde::encode(val)
|
||||||
|
.map_err(|e| DeSerError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
s.read_to_end(&mut buf)
|
s.read_to_end(&mut buf)
|
||||||
.map_err(|e| rustbreak::error::DeSerError::Internal(e.to_string()))?;
|
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||||
let (val, _version) =
|
let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
|
||||||
native_model::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
.map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||||
Ok(val)
|
Ok(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,6 +47,8 @@ pub type DatabaseInterface =
|
|||||||
|
|
||||||
pub trait DatabaseImpls {
|
pub trait DatabaseImpls {
|
||||||
fn set_up_database() -> DatabaseInterface;
|
fn set_up_database() -> DatabaseInterface;
|
||||||
|
fn database_is_set_up(&self) -> bool;
|
||||||
|
fn fetch_base_url(&self) -> Url;
|
||||||
}
|
}
|
||||||
impl DatabaseImpls for DatabaseInterface {
|
impl DatabaseImpls for DatabaseInterface {
|
||||||
fn set_up_database() -> DatabaseInterface {
|
fn set_up_database() -> DatabaseInterface {
|
||||||
@ -66,7 +73,7 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let default = Database::new(games_base_dir, None);
|
let default = Database::new(games_base_dir, None, cache_dir);
|
||||||
debug!(
|
debug!(
|
||||||
"Creating database at path {}",
|
"Creating database at path {}",
|
||||||
db_path.as_os_str().to_str().unwrap()
|
db_path.as_os_str().to_str().unwrap()
|
||||||
@ -74,6 +81,15 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn database_is_set_up(&self) -> bool {
|
||||||
|
!self.borrow_data().unwrap().base_url.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_base_url(&self) -> Url {
|
||||||
|
let handle = self.borrow_data().unwrap();
|
||||||
|
Url::parse(&handle.base_url).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
|
||||||
@ -96,14 +112,15 @@ fn handle_invalid_database(
|
|||||||
let db = Database::new(
|
let db = Database::new(
|
||||||
games_base_dir.into_os_string().into_string().unwrap(),
|
games_base_dir.into_os_string().into_string().unwrap(),
|
||||||
Some(new_path),
|
Some(new_path),
|
||||||
|
cache_dir,
|
||||||
);
|
);
|
||||||
|
|
||||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||||
}
|
}
|
||||||
|
|
||||||
// To automatically save the database upon drop
|
// To automatically save the database upon drop
|
||||||
pub struct DBRead<'a>(pub(crate) RwLockReadGuard<'a, Database>);
|
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
|
||||||
pub struct DBWrite<'a>(pub(crate) ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
|
||||||
impl<'a> Deref for DBWrite<'a> {
|
impl<'a> Deref for DBWrite<'a> {
|
||||||
type Target = Database;
|
type Target = Database;
|
||||||
|
|
||||||
@ -138,3 +155,23 @@ impl Drop for DBWrite<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
|
||||||
|
match DB.borrow_data() {
|
||||||
|
Ok(data) => DBRead(data),
|
||||||
|
Err(e) => {
|
||||||
|
error!("database borrow failed with error {e}");
|
||||||
|
panic!("database borrow failed with error {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
|
||||||
|
match DB.borrow_data_mut() {
|
||||||
|
Ok(data) => DBWrite(ManuallyDrop::new(data)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("database borrow mut failed with error {e}");
|
||||||
|
panic!("database borrow mut failed with error {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod db;
|
||||||
|
pub mod debug;
|
||||||
|
pub mod models;
|
||||||
pub mod scan;
|
pub mod scan;
|
||||||
@ -1,47 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* NEXT BREAKING CHANGE
|
||||||
|
*
|
||||||
|
* UPDATE DATABASE TO USE RPMSERDENAMED
|
||||||
|
*
|
||||||
|
* WE CAN'T DELETE ANY FIELDS
|
||||||
|
*/
|
||||||
pub mod data {
|
pub mod data {
|
||||||
use std::{hash::Hash, path::PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|
||||||
use native_model::native_model;
|
use native_model::native_model;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// NOTE: Within each version, you should NEVER use these types.
|
|
||||||
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
|
|
||||||
|
|
||||||
pub type GameVersion = v1::GameVersion;
|
pub type GameVersion = v1::GameVersion;
|
||||||
pub type Database = v4::Database;
|
pub type Database = v3::Database;
|
||||||
pub type Settings = v1::Settings;
|
pub type Settings = v1::Settings;
|
||||||
pub type DatabaseAuth = v1::DatabaseAuth;
|
pub type DatabaseAuth = v1::DatabaseAuth;
|
||||||
|
|
||||||
pub type GameDownloadStatus = v2::GameDownloadStatus;
|
pub type GameDownloadStatus = v2::GameDownloadStatus;
|
||||||
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
||||||
/**
|
|
||||||
* Need to be universally accessible by the ID, and the version is just a couple sprinkles on top
|
|
||||||
*/
|
|
||||||
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
||||||
pub type DownloadType = v1::DownloadType;
|
pub type DownloadType = v1::DownloadType;
|
||||||
pub type DatabaseApplications = v4::DatabaseApplications;
|
pub type DatabaseApplications = v2::DatabaseApplications;
|
||||||
// pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
impl PartialEq for DownloadableMetadata {
|
pub mod v1 {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
use crate::process::process_manager::Platform;
|
||||||
self.id == other.id && self.download_type == other.download_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Hash for DownloadableMetadata {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.id.hash(state);
|
|
||||||
self.download_type.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod v1 {
|
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use crate::process::Platform;
|
|
||||||
|
|
||||||
use super::{Deserialize, Serialize, native_model};
|
use super::{Deserialize, Serialize, native_model};
|
||||||
|
|
||||||
fn default_template() -> String {
|
fn default_template() -> String {
|
||||||
@ -127,7 +116,6 @@ pub mod data {
|
|||||||
// Stuff that shouldn't be synced to disk
|
// Stuff that shouldn't be synced to disk
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub enum ApplicationTransientStatus {
|
pub enum ApplicationTransientStatus {
|
||||||
Queued { version_name: String },
|
|
||||||
Downloading { version_name: String },
|
Downloading { version_name: String },
|
||||||
Uninstalling {},
|
Uninstalling {},
|
||||||
Updating { version_name: String },
|
Updating { version_name: String },
|
||||||
@ -156,7 +144,7 @@ pub mod data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
#[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
#[derive(Debug, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownloadableMetadata {
|
pub struct DownloadableMetadata {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -186,21 +174,22 @@ pub mod data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod v2 {
|
pub mod v2 {
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use crate::runtime_models::Game;
|
use super::{
|
||||||
|
ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata,
|
||||||
|
GameVersion, Serialize, Settings, native_model, v1,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{Deserialize, Serialize, native_model, v1};
|
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
|
|
||||||
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::Database)]
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub settings: v1::Settings,
|
pub settings: Settings,
|
||||||
pub auth: Option<v1::DatabaseAuth>,
|
pub auth: Option<DatabaseAuth>,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub applications: v1::DatabaseApplications,
|
pub applications: v1::DatabaseApplications,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@ -209,7 +198,7 @@ pub mod data {
|
|||||||
pub compat_info: Option<DatabaseCompatInfo>,
|
pub compat_info: Option<DatabaseCompatInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
#[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
|
||||||
pub struct DatabaseCompatInfo {
|
pub struct DatabaseCompatInfo {
|
||||||
@ -232,7 +221,7 @@ pub mod data {
|
|||||||
// Strings are version names for a particular game
|
// Strings are version names for a particular game
|
||||||
#[derive(Serialize, Clone, Deserialize, Debug)]
|
#[derive(Serialize, Clone, Deserialize, Debug)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::GameDownloadStatus)]
|
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
pub enum GameDownloadStatus {
|
pub enum GameDownloadStatus {
|
||||||
Remote {},
|
Remote {},
|
||||||
SetupRequired {
|
SetupRequired {
|
||||||
@ -272,16 +261,17 @@ pub mod data {
|
|||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Clone, Deserialize, Default)]
|
#[derive(Serialize, Clone, Deserialize, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from=v1::DatabaseApplications)]
|
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
pub struct DatabaseApplications {
|
pub struct DatabaseApplications {
|
||||||
pub install_dirs: Vec<PathBuf>,
|
pub install_dirs: Vec<PathBuf>,
|
||||||
|
// Guaranteed to exist if the game also exists in the app state map
|
||||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||||
pub game_versions: HashMap<String, HashMap<String, v1::GameVersion>>,
|
|
||||||
pub installed_game_version: HashMap<String, v1::DownloadableMetadata>,
|
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||||
|
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub transient_statuses:
|
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
||||||
HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
|
|
||||||
}
|
}
|
||||||
impl From<v1::DatabaseApplications> for DatabaseApplications {
|
impl From<v1::DatabaseApplications> for DatabaseApplications {
|
||||||
fn from(value: v1::DatabaseApplications) -> Self {
|
fn from(value: v1::DatabaseApplications) -> Self {
|
||||||
@ -302,19 +292,22 @@ pub mod data {
|
|||||||
mod v3 {
|
mod v3 {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::{Deserialize, Serialize, native_model, v1, v2};
|
use super::{
|
||||||
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde, from = v2::Database)]
|
DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize,
|
||||||
|
Settings, native_model, v2,
|
||||||
|
};
|
||||||
|
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub settings: v1::Settings,
|
pub settings: Settings,
|
||||||
pub auth: Option<v1::DatabaseAuth>,
|
pub auth: Option<DatabaseAuth>,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub applications: v2::DatabaseApplications,
|
pub applications: DatabaseApplications,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub prev_database: Option<PathBuf>,
|
pub prev_database: Option<PathBuf>,
|
||||||
pub cache_dir: PathBuf,
|
pub cache_dir: PathBuf,
|
||||||
pub compat_info: Option<v2::DatabaseCompatInfo>,
|
pub compat_info: Option<DatabaseCompatInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<v2::Database> for Database {
|
impl From<v2::Database> for Database {
|
||||||
@ -332,72 +325,26 @@ pub mod data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod v4 {
|
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
|
||||||
use drop_library::libraries::LibraryProviderIdentifier;
|
|
||||||
use drop_native_library::impls::DropNativeLibraryProvider;
|
|
||||||
use serde_with::serde_as;
|
|
||||||
use crate::models::data::v3;
|
|
||||||
use super::{Deserialize, Serialize, native_model, v1, v2};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub enum Library {
|
|
||||||
NativeLibrary(DropNativeLibraryProvider),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[native_model(id = 3, version = 4, with = native_model::rmp_serde_1_3::RmpSerde, from=v2::DatabaseApplications)]
|
|
||||||
pub struct DatabaseApplications {
|
|
||||||
pub install_dirs: Vec<PathBuf>,
|
|
||||||
pub libraries: HashMap<LibraryProviderIdentifier, Library>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub transient_statuses:
|
|
||||||
HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<v2::DatabaseApplications> for DatabaseApplications {
|
|
||||||
fn from(value: v2::DatabaseApplications) -> Self {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[native_model(id = 1, version = 4, with = native_model::rmp_serde_1_3::RmpSerde, from = v3::Database)]
|
|
||||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
|
||||||
pub struct Database {
|
|
||||||
#[serde(default)]
|
|
||||||
pub settings: v1::Settings,
|
|
||||||
pub drop_applications: DatabaseApplications,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub prev_database: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<v3::Database> for Database {
|
|
||||||
fn from(value: v3::Database) -> Self {
|
|
||||||
Database {
|
|
||||||
settings: value.settings,
|
|
||||||
drop_applications: value.applications.into(),
|
|
||||||
prev_database: value.prev_database,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn new<T: Into<PathBuf>>(
|
pub fn new<T: Into<PathBuf>>(
|
||||||
games_base_dir: T,
|
games_base_dir: T,
|
||||||
prev_database: Option<PathBuf>,
|
prev_database: Option<PathBuf>,
|
||||||
|
cache_dir: PathBuf,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
drop_applications: DatabaseApplications {
|
applications: DatabaseApplications {
|
||||||
install_dirs: vec![games_base_dir.into()],
|
install_dirs: vec![games_base_dir.into()],
|
||||||
libraries: HashMap::new(),
|
game_statuses: HashMap::new(),
|
||||||
|
game_versions: HashMap::new(),
|
||||||
|
installed_game_version: HashMap::new(),
|
||||||
transient_statuses: HashMap::new(),
|
transient_statuses: HashMap::new(),
|
||||||
},
|
},
|
||||||
prev_database,
|
prev_database,
|
||||||
|
base_url: String::new(),
|
||||||
|
auth: None,
|
||||||
settings: Settings::default(),
|
settings: Settings::default(),
|
||||||
|
cache_dir,
|
||||||
|
compat_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,18 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use drop_database::{borrow_db_mut_checked, drop_data::{DropData, DROP_DATA_PATH}, models::data::{DownloadType, DownloadableMetadata}};
|
|
||||||
use drop_native_library::library::set_partially_installed_db;
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{
|
||||||
|
db::borrow_db_mut_checked,
|
||||||
|
models::data::v1::{DownloadType, DownloadableMetadata},
|
||||||
|
},
|
||||||
|
games::{
|
||||||
|
downloads::drop_data::{v1::DropData, DROP_DATA_PATH},
|
||||||
|
library::set_partially_installed_db,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn scan_install_dirs() {
|
pub fn scan_install_dirs() {
|
||||||
let mut db_lock = borrow_db_mut_checked();
|
let mut db_lock = borrow_db_mut_checked();
|
||||||
for install_dir in db_lock.applications.install_dirs.clone() {
|
for install_dir in db_lock.applications.install_dirs.clone() {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use drop_database::models::data::DownloadableMetadata;
|
use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
|
|||||||
@ -7,13 +7,13 @@ use std::{
|
|||||||
thread::{JoinHandle, spawn},
|
thread::{JoinHandle, spawn},
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::models::data::DownloadableMetadata;
|
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
download_manager_frontend::DownloadStatus, events::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}
|
database::models::data::DownloadableMetadata,
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -29,6 +29,43 @@ use super::{
|
|||||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||||
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
|
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Welcome to the download manager, the most overengineered, glorious piece of bullshit.
|
||||||
|
|
||||||
|
The download manager takes a queue of ids and their associated
|
||||||
|
DownloadAgents, and then, one-by-one, executes them. It provides an interface
|
||||||
|
to interact with the currently downloading agent, and manage the queue.
|
||||||
|
|
||||||
|
When the DownloadManager is initialised, it is designed to provide a reference
|
||||||
|
which can be used to provide some instructions (the DownloadManagerInterface),
|
||||||
|
but other than that, it runs without any sort of interruptions.
|
||||||
|
|
||||||
|
It does this by opening up two data structures. Primarily is the command_receiver,
|
||||||
|
and mpsc (multi-channel-single-producer) which allows commands to be sent from
|
||||||
|
the Interface, and queued up for the Manager to process.
|
||||||
|
|
||||||
|
These have been mapped in the DownloadManagerSignal docs.
|
||||||
|
|
||||||
|
The other way to interact with the DownloadManager is via the donwload_queue,
|
||||||
|
which is just a collection of ids which may be rearranged to suit
|
||||||
|
whichever download queue order is required.
|
||||||
|
|
||||||
|
+----------------------------------------------------------------------------+
|
||||||
|
| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
|
||||||
|
| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
|
||||||
|
| WHICH HAS NOT BEEN ACCOUNTED FOR |
|
||||||
|
+----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
This download queue does not actually own any of the DownloadAgents. It is
|
||||||
|
simply an id-based reference system. The actual Agents are stored in the
|
||||||
|
download_agent_registry HashMap, as ordering is no issue here. This is why
|
||||||
|
appending or removing from the download_queue must be done via signals.
|
||||||
|
|
||||||
|
Behold, my madness - quexeky
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
pub struct DownloadManagerBuilder {
|
pub struct DownloadManagerBuilder {
|
||||||
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
|
||||||
download_queue: Queue,
|
download_queue: Queue,
|
||||||
@ -38,6 +75,7 @@ pub struct DownloadManagerBuilder {
|
|||||||
status: Arc<Mutex<DownloadManagerStatus>>,
|
status: Arc<Mutex<DownloadManagerStatus>>,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
|
|
||||||
|
current_download_agent: Option<DownloadAgent>, // Should be the only download agent in the map with the "Go" flag
|
||||||
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
current_download_thread: Mutex<Option<JoinHandle<()>>>,
|
||||||
active_control_flag: Option<DownloadThreadControl>,
|
active_control_flag: Option<DownloadThreadControl>,
|
||||||
}
|
}
|
||||||
@ -57,6 +95,7 @@ impl DownloadManagerBuilder {
|
|||||||
progress: active_progress.clone(),
|
progress: active_progress.clone(),
|
||||||
app_handle,
|
app_handle,
|
||||||
|
|
||||||
|
current_download_agent: None,
|
||||||
current_download_thread: Mutex::new(None),
|
current_download_thread: Mutex::new(None),
|
||||||
active_control_flag: None,
|
active_control_flag: None,
|
||||||
};
|
};
|
||||||
@ -82,6 +121,7 @@ impl DownloadManagerBuilder {
|
|||||||
fn cleanup_current_download(&mut self) {
|
fn cleanup_current_download(&mut self) {
|
||||||
self.active_control_flag = None;
|
self.active_control_flag = None;
|
||||||
*self.progress.lock().unwrap() = None;
|
*self.progress.lock().unwrap() = 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();
|
||||||
|
|
||||||
@ -157,7 +197,7 @@ impl DownloadManagerBuilder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
download_agent.on_queued(&self.app_handle);
|
download_agent.on_initialised(&self.app_handle);
|
||||||
self.download_queue.append(meta.clone());
|
self.download_queue.append(meta.clone());
|
||||||
self.download_agent_registry.insert(meta, download_agent);
|
self.download_agent_registry.insert(meta, download_agent);
|
||||||
|
|
||||||
@ -176,13 +216,19 @@ impl DownloadManagerBuilder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.current_download_agent.is_some()
|
||||||
|
&& self.download_queue.read().front().unwrap()
|
||||||
|
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
debug!("current download queue: {:?}", self.download_queue.read());
|
debug!("current download queue: {:?}", self.download_queue.read());
|
||||||
|
|
||||||
let agent_data = if let Some(agent_data) = self.download_queue.read().front() {
|
// Should always be Some if the above two statements keep going
|
||||||
agent_data.clone()
|
let agent_data = self.download_queue.read().front().unwrap().clone();
|
||||||
} else {
|
|
||||||
return;
|
info!("starting download for {agent_data:?}");
|
||||||
};
|
|
||||||
|
|
||||||
let download_agent = self
|
let download_agent = self
|
||||||
.download_agent_registry
|
.download_agent_registry
|
||||||
@ -190,22 +236,8 @@ impl DownloadManagerBuilder {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let status = download_agent.status();
|
|
||||||
|
|
||||||
// This download is already going
|
|
||||||
if status != DownloadStatus::Queued {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all others are marked as queued
|
|
||||||
for agent in self.download_agent_registry.values() {
|
|
||||||
if agent.metadata() != agent_data && agent.status() != DownloadStatus::Queued {
|
|
||||||
agent.on_queued(&self.app_handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("starting download for {agent_data:?}");
|
|
||||||
self.active_control_flag = Some(download_agent.control_flag());
|
self.active_control_flag = Some(download_agent.control_flag());
|
||||||
|
self.current_download_agent = Some(download_agent.clone());
|
||||||
|
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
@ -278,8 +310,8 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
|
||||||
debug!("got signal Completed");
|
debug!("got signal Completed");
|
||||||
if let Some(interface) = self.download_queue.read().front()
|
if let Some(interface) = &self.current_download_agent
|
||||||
&& interface == &meta
|
&& interface.metadata() == meta
|
||||||
{
|
{
|
||||||
self.remove_and_cleanup_front_download(&meta);
|
self.remove_and_cleanup_front_download(&meta);
|
||||||
}
|
}
|
||||||
@ -289,13 +321,11 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
|
||||||
debug!("got signal Error");
|
debug!("got signal Error");
|
||||||
if let Some(metadata) = self.download_queue.read().front()
|
if let Some(current_agent) = self.current_download_agent.clone() {
|
||||||
&& let Some(current_agent) = self.download_agent_registry.get(metadata)
|
|
||||||
{
|
|
||||||
current_agent.on_error(&self.app_handle, &error);
|
current_agent.on_error(&self.app_handle, &error);
|
||||||
|
|
||||||
self.stop_and_wait_current_download();
|
self.stop_and_wait_current_download();
|
||||||
self.remove_and_cleanup_front_download(metadata);
|
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||||
}
|
}
|
||||||
self.push_ui_queue_update();
|
self.push_ui_queue_update();
|
||||||
self.set_status(DownloadManagerStatus::Error);
|
self.set_status(DownloadManagerStatus::Error);
|
||||||
@ -303,11 +333,8 @@ impl DownloadManagerBuilder {
|
|||||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||||
debug!("got signal Cancel");
|
debug!("got signal Cancel");
|
||||||
|
|
||||||
// If the current download is the one we're tryna cancel
|
if let Some(current_download) = &self.current_download_agent {
|
||||||
if let Some(current_metadata) = self.download_queue.read().front()
|
if ¤t_download.metadata() == meta {
|
||||||
&& current_metadata == meta
|
|
||||||
&& let Some(current_download) = self.download_agent_registry.get(current_metadata)
|
|
||||||
{
|
|
||||||
self.set_status(DownloadManagerStatus::Paused);
|
self.set_status(DownloadManagerStatus::Paused);
|
||||||
current_download.on_cancelled(&self.app_handle);
|
current_download.on_cancelled(&self.app_handle);
|
||||||
self.stop_and_wait_current_download();
|
self.stop_and_wait_current_download();
|
||||||
@ -315,10 +342,9 @@ impl DownloadManagerBuilder {
|
|||||||
self.download_queue.pop_front();
|
self.download_queue.pop_front();
|
||||||
|
|
||||||
self.cleanup_current_download();
|
self.cleanup_current_download();
|
||||||
self.download_agent_registry.remove(meta);
|
|
||||||
debug!("current download queue: {:?}", self.download_queue.read());
|
debug!("current download queue: {:?}", self.download_queue.read());
|
||||||
}
|
}
|
||||||
// else just cancel it
|
// TODO: Collapse these two into a single if statement somehow
|
||||||
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||||
let index = self.download_queue.get_by_meta(meta);
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
@ -332,7 +358,19 @@ impl DownloadManagerBuilder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.sender.send(DownloadManagerSignal::Go).unwrap();
|
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
|
||||||
|
let index = self.download_queue.get_by_meta(meta);
|
||||||
|
if let Some(index) = index {
|
||||||
|
download_agent.on_cancelled(&self.app_handle);
|
||||||
|
let _ = self.download_queue.edit().remove(index).unwrap();
|
||||||
|
let removed = self.download_agent_registry.remove(meta);
|
||||||
|
debug!(
|
||||||
|
"removed {:?} from queue {:?}",
|
||||||
|
removed.map(|x| x.metadata()),
|
||||||
|
self.download_queue.read()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.push_ui_queue_update();
|
self.push_ui_queue_update();
|
||||||
}
|
}
|
||||||
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
fn push_ui_stats_update(&self, kbs: usize, time: usize) {
|
||||||
@ -9,11 +9,14 @@ use std::{
|
|||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::models::data::DownloadableMetadata;
|
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::models::data::DownloadableMetadata,
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
||||||
util::queue::Queue,
|
util::queue::Queue,
|
||||||
@ -59,7 +62,7 @@ impl Serialize for DownloadManagerStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
pub enum DownloadStatus {
|
pub enum DownloadStatus {
|
||||||
Queued,
|
Queued,
|
||||||
Downloading,
|
Downloading,
|
||||||
@ -1,20 +1,17 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use drop_database::models::data::DownloadableMetadata;
|
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::models::data::DownloadableMetadata,
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_frontend::DownloadStatus,
|
download_manager_frontend::DownloadStatus,
|
||||||
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloadables are responsible for managing their specific object's download state
|
|
||||||
* e.g, the GameDownloadAgent is responsible for pushing game updates
|
|
||||||
*
|
|
||||||
* But the download manager manages the queue state
|
|
||||||
*/
|
|
||||||
pub trait Downloadable: Send + Sync {
|
pub trait Downloadable: Send + Sync {
|
||||||
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||||
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
fn validate(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
|
||||||
@ -23,7 +20,7 @@ pub trait Downloadable: Send + Sync {
|
|||||||
fn control_flag(&self) -> DownloadThreadControl;
|
fn control_flag(&self) -> DownloadThreadControl;
|
||||||
fn status(&self) -> DownloadStatus;
|
fn status(&self) -> DownloadStatus;
|
||||||
fn metadata(&self) -> DownloadableMetadata;
|
fn metadata(&self) -> DownloadableMetadata;
|
||||||
fn on_queued(&self, app_handle: &AppHandle);
|
fn on_initialised(&self, app_handle: &AppHandle);
|
||||||
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError);
|
||||||
fn on_complete(&self, app_handle: &AppHandle);
|
fn on_complete(&self, app_handle: &AppHandle);
|
||||||
fn on_cancelled(&self, app_handle: &AppHandle);
|
fn on_cancelled(&self, app_handle: &AppHandle);
|
||||||
@ -1 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod download_manager_builder;
|
||||||
|
pub mod download_manager_frontend;
|
||||||
|
pub mod downloadable;
|
||||||
|
pub mod util;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use std::{
|
|||||||
use atomic_instant_full::AtomicInstant;
|
use atomic_instant_full::AtomicInstant;
|
||||||
use throttle_my_fn::throttle;
|
use throttle_my_fn::throttle;
|
||||||
|
|
||||||
use crate::download_manager_frontend::DownloadManagerSignal;
|
use crate::download_manager::download_manager_frontend::DownloadManagerSignal;
|
||||||
|
|
||||||
use super::rolling_progress_updates::RollingProgressWindow;
|
use super::rolling_progress_updates::RollingProgressWindow;
|
||||||
|
|
||||||
@ -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<1000>,
|
rolling: RollingProgressWindow<1>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -120,7 +120,7 @@ pub fn calculate_update(progress: &ProgressObject) {
|
|||||||
let last_update_time = progress
|
let last_update_time = progress
|
||||||
.last_update_time
|
.last_update_time
|
||||||
.swap(Instant::now(), Ordering::SeqCst);
|
.swap(Instant::now(), Ordering::SeqCst);
|
||||||
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis_f64();
|
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
|
||||||
|
|
||||||
let current_bytes_downloaded = progress.sum();
|
let current_bytes_downloaded = progress.sum();
|
||||||
let max = progress.get_max();
|
let max = progress.get_max();
|
||||||
@ -128,17 +128,17 @@ 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) as f64;
|
let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update);
|
||||||
|
|
||||||
let kilobytes_per_second = bytes_since_last_update / time_since_last_update;
|
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
|
||||||
|
|
||||||
let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes
|
let bytes_remaining = max.saturating_sub(current_bytes_downloaded); // bytes
|
||||||
|
|
||||||
progress.update_window(kilobytes_per_second as usize);
|
progress.update_window(kilobytes_per_second);
|
||||||
push_update(progress, bytes_remaining);
|
push_update(progress, bytes_remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[throttle(1, Duration::from_millis(250))]
|
#[throttle(1, Duration::from_millis(500))]
|
||||||
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
|
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
|
||||||
let average_speed = progress.rolling.get_average();
|
let average_speed = progress.rolling.get_average();
|
||||||
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
|
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
|
||||||
@ -3,7 +3,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex, MutexGuard},
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::models::data::DownloadableMetadata;
|
use crate::database::models::data::DownloadableMetadata;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
Arc,
|
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -22,22 +22,17 @@ impl<const S: usize> RollingProgressWindow<S> {
|
|||||||
}
|
}
|
||||||
pub fn get_average(&self) -> usize {
|
pub fn get_average(&self) -> usize {
|
||||||
let current = self.current.load(Ordering::SeqCst);
|
let current = self.current.load(Ordering::SeqCst);
|
||||||
let valid = self
|
self.window
|
||||||
.window
|
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(i, _)| i < ¤t)
|
.filter(|(i, _)| i < ¤t)
|
||||||
.map(|(_, x)| x.load(Ordering::Acquire))
|
.map(|(_, x)| x.load(Ordering::Acquire))
|
||||||
.collect::<Vec<usize>>();
|
.sum::<usize>()
|
||||||
let amount = valid.len();
|
/ S
|
||||||
let sum = valid.into_iter().sum::<usize>();
|
|
||||||
|
|
||||||
sum / amount
|
|
||||||
}
|
}
|
||||||
pub fn reset(&self) {
|
pub fn reset(&self) {
|
||||||
self.window
|
self.window
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|x| x.store(0, Ordering::Release));
|
.for_each(|x| x.store(0, Ordering::Release));
|
||||||
self.current.store(0, Ordering::Release);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ServerError {
|
pub struct DropServerError {
|
||||||
pub status_code: usize,
|
pub status_code: usize,
|
||||||
pub status_message: String,
|
pub status_message: String,
|
||||||
// pub message: String,
|
// pub message: String,
|
||||||
@ -8,7 +8,7 @@ use http::StatusCode;
|
|||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
use url::ParseError;
|
use url::ParseError;
|
||||||
|
|
||||||
use super::drop_server_error::ServerError;
|
use super::drop_server_error::DropServerError;
|
||||||
|
|
||||||
#[derive(Debug, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum RemoteAccessError {
|
pub enum RemoteAccessError {
|
||||||
@ -18,7 +18,7 @@ pub enum RemoteAccessError {
|
|||||||
InvalidEndpoint,
|
InvalidEndpoint,
|
||||||
HandshakeFailed(String),
|
HandshakeFailed(String),
|
||||||
GameNotFound(String),
|
GameNotFound(String),
|
||||||
InvalidResponse(ServerError),
|
InvalidResponse(DropServerError),
|
||||||
UnparseableResponse(String),
|
UnparseableResponse(String),
|
||||||
ManifestDownloadFailed(StatusCode, String),
|
ManifestDownloadFailed(StatusCode, String),
|
||||||
OutOfSync,
|
OutOfSync,
|
||||||
@ -1,10 +1,10 @@
|
|||||||
use bitcode::{Decode, Encode};
|
|
||||||
// use drop_database::runtime_models::Game;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::games::library::Game;
|
||||||
|
|
||||||
pub type Collections = Vec<Collection>;
|
pub type Collections = Vec<Collection>;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Collection {
|
pub struct Collection {
|
||||||
id: String,
|
id: String,
|
||||||
@ -14,7 +14,7 @@ pub struct Collection {
|
|||||||
entries: Vec<CollectionObject>,
|
entries: Vec<CollectionObject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CollectionObject {
|
pub struct CollectionObject {
|
||||||
collection_id: String,
|
collection_id: String,
|
||||||
@ -1,28 +1,22 @@
|
|||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
|
||||||
use drop_native_library::collections::{Collection, Collections};
|
|
||||||
use drop_remote::{
|
|
||||||
auth::generate_authorization_header, cache::{cache_object, get_cached_object}, requests::{generate_url, make_authenticated_get}, utils::DROP_CLIENT_ASYNC
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[tauri::command]
|
use crate::{
|
||||||
pub async fn fetch_collections(
|
error::remote_access_error::RemoteAccessError,
|
||||||
hard_refresh: Option<bool>,
|
remote::{
|
||||||
) -> Result<Collections, RemoteAccessError> {
|
auth::generate_authorization_header,
|
||||||
let do_hard_refresh = hard_refresh.unwrap_or(false);
|
requests::{generate_url, make_authenticated_get},
|
||||||
if !do_hard_refresh && let Ok(cached_response) = get_cached_object::<Collections>("collections")
|
utils::DROP_CLIENT_ASYNC,
|
||||||
{
|
},
|
||||||
return Ok(cached_response);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
use super::collection::{Collection, Collections};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
||||||
let response =
|
let response =
|
||||||
make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?;
|
make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?;
|
||||||
|
|
||||||
let collections: Collections = response.json().await?;
|
Ok(response.json().await?)
|
||||||
|
|
||||||
cache_object("collections", &collections)?;
|
|
||||||
|
|
||||||
Ok(collections)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -96,8 +90,7 @@ pub async fn delete_game_in_collection(
|
|||||||
.delete(url)
|
.delete(url)
|
||||||
.header("Authorization", generate_authorization_header())
|
.header("Authorization", generate_authorization_header())
|
||||||
.json(&json!({"id": game_id}))
|
.json(&json!({"id": game_id}))
|
||||||
.send()
|
.send().await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
2
src-tauri/src/games/collections/mod.rs
Normal file
2
src-tauri/src/games/collections/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod collection;
|
||||||
|
pub mod commands;
|
||||||
76
src-tauri/src/games/commands.rs
Normal file
76
src-tauri/src/games/commands.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
database::{
|
||||||
|
db::borrow_db_checked,
|
||||||
|
models::data::GameVersion,
|
||||||
|
},
|
||||||
|
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
|
||||||
|
games::library::{
|
||||||
|
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
|
||||||
|
uninstall_game_logic,
|
||||||
|
},
|
||||||
|
offline,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
library::{
|
||||||
|
FetchGameStruct, Game, fetch_game_logic, fetch_game_version_options_logic,
|
||||||
|
fetch_library_logic,
|
||||||
|
},
|
||||||
|
state::{GameStatusManager, GameStatusWithTransient},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn fetch_library(
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
|
offline!(
|
||||||
|
state,
|
||||||
|
fetch_library_logic,
|
||||||
|
fetch_library_logic_offline,
|
||||||
|
state
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn fetch_game(
|
||||||
|
game_id: String,
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
|
offline!(
|
||||||
|
state,
|
||||||
|
fetch_game_logic,
|
||||||
|
fetch_game_logic_offline,
|
||||||
|
game_id,
|
||||||
|
state
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
||||||
|
let db_handle = borrow_db_checked();
|
||||||
|
GameStatusManager::fetch_state(&id, &db_handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
||||||
|
let meta = match get_current_meta(&game_id) {
|
||||||
|
Some(data) => data,
|
||||||
|
None => return Err(LibraryError::MetaNotFound(game_id)),
|
||||||
|
};
|
||||||
|
uninstall_game_logic(meta, &app_handle);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn fetch_game_version_options(
|
||||||
|
game_id: String,
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||||
|
fetch_game_version_options_logic(game_id, state).await
|
||||||
|
}
|
||||||
@ -3,11 +3,16 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, models::data::GameDownloadStatus};
|
|
||||||
use drop_downloads::downloadable::Downloadable;
|
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
database::{
|
||||||
|
db::borrow_db_checked,
|
||||||
|
models::data::GameDownloadStatus,
|
||||||
|
},
|
||||||
|
download_manager::downloadable::Downloadable,
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::download_agent::GameDownloadAgent;
|
use super::download_agent::GameDownloadAgent;
|
||||||
|
|
||||||
@ -16,7 +21,7 @@ pub async 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().clone() };
|
||||||
|
|
||||||
@ -38,7 +43,7 @@ pub async fn download_game(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn resume_download(
|
pub async 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
|
||||||
@ -1,22 +1,29 @@
|
|||||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked};
|
use crate::auth::generate_authorization_header;
|
||||||
use drop_database::drop_data::DropData;
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
use drop_database::models::data::{ApplicationTransientStatus, DownloadType, DownloadableMetadata};
|
use crate::database::models::data::{
|
||||||
use drop_downloads::download_manager_frontend::{DownloadManagerSignal, DownloadStatus};
|
ApplicationTransientStatus, DownloadType, DownloadableMetadata,
|
||||||
use drop_downloads::downloadable::Downloadable;
|
};
|
||||||
use drop_downloads::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
use crate::download_manager::download_manager_frontend::{DownloadManagerSignal, DownloadStatus};
|
||||||
use drop_downloads::util::progress_object::{ProgressHandle, ProgressObject};
|
use crate::download_manager::downloadable::Downloadable;
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
use crate::download_manager::util::download_thread_control_flag::{
|
||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
DownloadThreadControl, DownloadThreadControlFlag,
|
||||||
use drop_native_library::library::{on_game_complete, push_game_update, set_partially_installed};
|
};
|
||||||
use drop_native_library::state::GameStatusManager;
|
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||||
use drop_process::utils::get_disk_available;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use drop_remote::auth::generate_authorization_header;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use drop_remote::requests::generate_url;
|
use crate::games::downloads::manifest::{
|
||||||
use drop_remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC};
|
DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody,
|
||||||
|
};
|
||||||
|
use crate::games::downloads::validate::validate_game_chunk;
|
||||||
|
use crate::games::library::{on_game_complete, push_game_update, set_partially_installed};
|
||||||
|
use crate::games::state::GameStatusManager;
|
||||||
|
use crate::process::utils::get_disk_available;
|
||||||
|
use crate::remote::requests::generate_url;
|
||||||
|
use crate::remote::utils::{DROP_CLIENT_ASYNC, 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, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::{OpenOptions, create_dir_all};
|
use std::fs::{create_dir_all, OpenOptions};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@ -26,15 +33,12 @@ use tauri::{AppHandle, Emitter};
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use rustix::fs::{FallocateFlags, fallocate};
|
use rustix::fs::{FallocateFlags, fallocate};
|
||||||
|
|
||||||
use crate::native_library::downloads::manifest::{DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody};
|
|
||||||
use crate::native_library::downloads::validate::validate_game_chunk;
|
|
||||||
|
|
||||||
use super::download_logic::download_game_bucket;
|
use super::download_logic::download_game_bucket;
|
||||||
|
use super::drop_data::DropData;
|
||||||
|
|
||||||
static RETRY_COUNT: usize = 3;
|
static RETRY_COUNT: usize = 3;
|
||||||
|
|
||||||
const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000;
|
const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000;
|
||||||
const MAX_FILES_PER_BUCKET: usize = (1024 / 4) - 1;
|
|
||||||
|
|
||||||
pub struct GameDownloadAgent {
|
pub struct GameDownloadAgent {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -79,8 +83,6 @@ impl GameDownloadAgent {
|
|||||||
let stored_manifest =
|
let stored_manifest =
|
||||||
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
|
||||||
|
|
||||||
let context_lock = stored_manifest.contexts.lock().unwrap().clone();
|
|
||||||
|
|
||||||
let result = Self {
|
let result = Self {
|
||||||
id,
|
id,
|
||||||
version,
|
version,
|
||||||
@ -103,14 +105,7 @@ impl GameDownloadAgent {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.values()
|
.values()
|
||||||
.map(|e| {
|
.map(|e| e.lengths.iter().sum::<usize>())
|
||||||
e.lengths
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(i, _)| *context_lock.get(&e.checksums[*i]).unwrap_or(&false))
|
|
||||||
.map(|(_, v)| v)
|
|
||||||
.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;
|
||||||
@ -303,8 +298,7 @@ impl GameDownloadAgent {
|
|||||||
drops: vec![],
|
drops: vec![],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (*current_bucket_size + length >= TARGET_BUCKET_SIZE
|
if *current_bucket_size + length >= TARGET_BUCKET_SIZE
|
||||||
|| current_bucket.drops.len() >= MAX_FILES_PER_BUCKET)
|
|
||||||
&& !current_bucket.drops.is_empty()
|
&& !current_bucket.drops.is_empty()
|
||||||
{
|
{
|
||||||
// Move current bucket into list and make a new one
|
// Move current bucket into list and make a new one
|
||||||
@ -373,8 +367,7 @@ impl GameDownloadAgent {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|e| &e.version)
|
.map(|e| &e.version)
|
||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
.into_iter()
|
.into_iter().cloned()
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
info!("downloading across these versions: {versions:?}");
|
info!("downloading across these versions: {versions:?}");
|
||||||
@ -467,7 +460,6 @@ impl GameDownloadAgent {
|
|||||||
ApplicationDownloadError::Communication(_)
|
ApplicationDownloadError::Communication(_)
|
||||||
| ApplicationDownloadError::Checksum
|
| ApplicationDownloadError::Checksum
|
||||||
| ApplicationDownloadError::Lock
|
| ApplicationDownloadError::Lock
|
||||||
| ApplicationDownloadError::IoError(_)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if i == RETRY_COUNT - 1 || !retry {
|
if i == RETRY_COUNT - 1 || !retry {
|
||||||
@ -634,17 +626,8 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_queued(&self, app_handle: &tauri::AppHandle) {
|
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
|
||||||
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
*self.status.lock().unwrap() = DownloadStatus::Queued;
|
||||||
let mut db_lock = borrow_db_mut_checked();
|
|
||||||
let status = ApplicationTransientStatus::Queued {
|
|
||||||
version_name: self.version.clone(),
|
|
||||||
};
|
|
||||||
db_lock
|
|
||||||
.applications
|
|
||||||
.transient_statuses
|
|
||||||
.insert(self.metadata(), status.clone());
|
|
||||||
push_game_update(app_handle, &self.id, None, (None, Some(status)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
fn on_error(&self, app_handle: &tauri::AppHandle, error: &ApplicationDownloadError) {
|
||||||
@ -653,7 +636,7 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
.emit("download_error", error.to_string())
|
.emit("download_error", error.to_string())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
error!("error while managing download: {error:?}");
|
error!("error while managing download: {error}");
|
||||||
|
|
||||||
let mut handle = borrow_db_mut_checked();
|
let mut handle = borrow_db_mut_checked();
|
||||||
handle
|
handle
|
||||||
@ -679,8 +662,15 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
|
||||||
info!("cancelled {}", self.id);
|
|
||||||
self.cancel(app_handle);
|
self.cancel(app_handle);
|
||||||
|
/*
|
||||||
|
on_game_incomplete(
|
||||||
|
&self.metadata(),
|
||||||
|
self.dropdata.base_path.to_string_lossy().to_string(),
|
||||||
|
app_handle,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(&self) -> DownloadStatus {
|
fn status(&self) -> DownloadStatus {
|
||||||
@ -1,12 +1,15 @@
|
|||||||
use drop_downloads::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
use crate::download_manager::util::download_thread_control_flag::{
|
||||||
use drop_downloads::util::progress_object::ProgressHandle;
|
DownloadThreadControl, DownloadThreadControlFlag,
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
};
|
||||||
use drop_errors::drop_server_error::ServerError;
|
use crate::download_manager::util::progress_object::ProgressHandle;
|
||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use drop_remote::auth::generate_authorization_header;
|
use crate::error::drop_server_error::DropServerError;
|
||||||
use drop_remote::requests::generate_url;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use drop_remote::utils::DROP_CLIENT_SYNC;
|
use crate::games::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop};
|
||||||
use log::{debug, info, warn};
|
use crate::remote::auth::generate_authorization_header;
|
||||||
|
use crate::remote::requests::generate_url;
|
||||||
|
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||||
|
use log::{info, warn};
|
||||||
use md5::{Context, Digest};
|
use md5::{Context, Digest};
|
||||||
use reqwest::blocking::Response;
|
use reqwest::blocking::Response;
|
||||||
|
|
||||||
@ -15,17 +18,13 @@ use std::io::Read;
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::native_library::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop};
|
|
||||||
|
|
||||||
static MAX_PACKET_LENGTH: usize = 4096 * 4;
|
static MAX_PACKET_LENGTH: usize = 4096 * 4;
|
||||||
static BUMP_SIZE: usize = 4096 * 16;
|
|
||||||
|
|
||||||
pub struct DropWriter<W: Write> {
|
pub struct DropWriter<W: Write> {
|
||||||
hasher: Context,
|
hasher: Context,
|
||||||
@ -80,8 +79,6 @@ pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
|
|||||||
pub drops: Vec<DownloadDrop>,
|
pub drops: Vec<DownloadDrop>,
|
||||||
pub destination: Vec<DropWriter<W>>,
|
pub destination: Vec<DropWriter<W>>,
|
||||||
pub control_flag: &'a DownloadThreadControl,
|
pub control_flag: &'a DownloadThreadControl,
|
||||||
#[allow(dead_code)]
|
|
||||||
progress: ProgressHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
impl<'a> DropDownloadPipeline<'a, Response, File> {
|
||||||
@ -99,7 +96,6 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
.try_collect()?,
|
.try_collect()?,
|
||||||
drops,
|
drops,
|
||||||
control_flag,
|
control_flag,
|
||||||
progress,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,24 +111,13 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
if drop.start != 0 {
|
if drop.start != 0 {
|
||||||
destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?;
|
destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?;
|
||||||
}
|
}
|
||||||
let mut last_bump = 0;
|
|
||||||
loop {
|
loop {
|
||||||
let size = MAX_PACKET_LENGTH.min(remaining);
|
let size = MAX_PACKET_LENGTH.min(remaining);
|
||||||
let size = self.source.read(&mut copy_buffer[0..size]).inspect_err(|_| {
|
self.source.read_exact(&mut copy_buffer[0..size])?;
|
||||||
info!("got error from {}", drop.filename);
|
|
||||||
})?;
|
|
||||||
remaining -= size;
|
remaining -= size;
|
||||||
last_bump += size;
|
|
||||||
|
|
||||||
destination.write_all(©_buffer[0..size])?;
|
destination.write_all(©_buffer[0..size])?;
|
||||||
|
|
||||||
if last_bump > BUMP_SIZE {
|
|
||||||
last_bump -= BUMP_SIZE;
|
|
||||||
if self.control_flag.get() == DownloadThreadControlFlag::Stop {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if remaining == 0 {
|
if remaining == 0 {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
@ -146,13 +131,6 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn debug_skip_checksum(self) {
|
|
||||||
self.destination
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|mut e| e.flush().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self) -> Result<Vec<Digest>, io::Error> {
|
fn finish(self) -> Result<Vec<Digest>, io::Error> {
|
||||||
let checksums = self
|
let checksums = self
|
||||||
.destination
|
.destination
|
||||||
@ -175,8 +153,6 @@ pub fn download_game_bucket(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let url = generate_url(&["/api/v2/client/chunk"], &[])
|
let url = generate_url(&["/api/v2/client/chunk"], &[])
|
||||||
@ -197,7 +173,7 @@ pub fn download_game_bucket(
|
|||||||
ApplicationDownloadError::Communication(RemoteAccessError::FetchError(e.into()))
|
ApplicationDownloadError::Communication(RemoteAccessError::FetchError(e.into()))
|
||||||
})?;
|
})?;
|
||||||
info!("{raw_res}");
|
info!("{raw_res}");
|
||||||
if let Ok(err) = serde_json::from_str::<ServerError>(&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),
|
||||||
));
|
));
|
||||||
@ -219,7 +195,9 @@ pub fn download_game_bucket(
|
|||||||
for (i, raw_length) in lengths.split(",").enumerate() {
|
for (i, raw_length) in lengths.split(",").enumerate() {
|
||||||
let length = raw_length.parse::<usize>().unwrap_or(0);
|
let length = raw_length.parse::<usize>().unwrap_or(0);
|
||||||
let Some(drop) = bucket.drops.get(i) else {
|
let Some(drop) = bucket.drops.get(i) else {
|
||||||
warn!("invalid number of Content-Lengths recieved: {i}, {lengths}");
|
warn!(
|
||||||
|
"invalid number of Content-Lengths recieved: {i}, {lengths}"
|
||||||
|
);
|
||||||
return Err(ApplicationDownloadError::DownloadError);
|
return Err(ApplicationDownloadError::DownloadError);
|
||||||
};
|
};
|
||||||
if drop.length != length {
|
if drop.length != length {
|
||||||
@ -231,10 +209,6 @@ pub fn download_game_bucket(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestep = start.elapsed().as_millis();
|
|
||||||
|
|
||||||
debug!("took {}ms to start downloading", timestep);
|
|
||||||
|
|
||||||
let mut pipeline =
|
let mut pipeline =
|
||||||
DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress)
|
DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress)
|
||||||
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
.map_err(|e| ApplicationDownloadError::IoError(Arc::new(e)))?;
|
||||||
@ -9,7 +9,7 @@ pub type DropData = v1::DropData;
|
|||||||
|
|
||||||
pub static DROP_DATA_PATH: &str = ".dropdata";
|
pub static DROP_DATA_PATH: &str = ".dropdata";
|
||||||
|
|
||||||
mod v1 {
|
pub mod v1 {
|
||||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
|
||||||
|
|
||||||
use native_model::native_model;
|
use native_model::native_model;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod download_agent;
|
pub mod download_agent;
|
||||||
mod download_logic;
|
mod download_logic;
|
||||||
|
pub mod drop_data;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
@ -3,12 +3,17 @@ use std::{
|
|||||||
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_downloads::util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressHandle};
|
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use md5::Context;
|
use md5::Context;
|
||||||
|
|
||||||
use crate::native_library::downloads::manifest::DropValidateContext;
|
use crate::{
|
||||||
|
download_manager::util::{
|
||||||
|
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
||||||
|
progress_object::ProgressHandle,
|
||||||
|
},
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
games::downloads::manifest::DropValidateContext,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn validate_game_chunk(
|
pub fn validate_game_chunk(
|
||||||
ctx: &DropValidateContext,
|
ctx: &DropValidateContext,
|
||||||
@ -1,34 +1,30 @@
|
|||||||
use std::fs::remove_dir_all;
|
use std::fs::remove_dir_all;
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::thread::spawn;
|
use std::thread::spawn;
|
||||||
|
|
||||||
use drop_database::borrow_db_checked;
|
|
||||||
use drop_database::borrow_db_mut_checked;
|
|
||||||
use drop_database::models::data::ApplicationTransientStatus;
|
|
||||||
use drop_database::models::data::Database;
|
|
||||||
use drop_database::models::data::DownloadableMetadata;
|
|
||||||
use drop_database::models::data::GameDownloadStatus;
|
|
||||||
use drop_database::models::data::GameVersion;
|
|
||||||
use drop_database::runtime_models::Game;
|
|
||||||
use drop_errors::drop_server_error::ServerError;
|
|
||||||
use drop_errors::library_error::LibraryError;
|
|
||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
|
||||||
use drop_remote::DropRemoteContext;
|
|
||||||
use drop_remote::auth::generate_authorization_header;
|
|
||||||
use drop_remote::cache::cache_object;
|
|
||||||
use drop_remote::cache::cache_object_db;
|
|
||||||
use drop_remote::cache::get_cached_object;
|
|
||||||
use drop_remote::cache::get_cached_object_db;
|
|
||||||
use drop_remote::requests::generate_url;
|
|
||||||
use drop_remote::utils::DROP_CLIENT_ASYNC;
|
|
||||||
use drop_remote::utils::DROP_CLIENT_SYNC;
|
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::Emitter as _;
|
use tauri::Emitter;
|
||||||
|
|
||||||
use crate::events::GameUpdateEvent;
|
use crate::AppState;
|
||||||
use crate::state::GameStatusManager;
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
|
||||||
use crate::state::GameStatusWithTransient;
|
use crate::database::models::data::Database;
|
||||||
|
use crate::database::models::data::{
|
||||||
|
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||||
|
};
|
||||||
|
use crate::download_manager::download_manager_frontend::DownloadStatus;
|
||||||
|
use crate::error::drop_server_error::DropServerError;
|
||||||
|
use crate::error::library_error::LibraryError;
|
||||||
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
|
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
|
use crate::remote::auth::generate_authorization_header;
|
||||||
|
use crate::remote::cache::cache_object_db;
|
||||||
|
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
|
||||||
|
use crate::remote::requests::generate_url;
|
||||||
|
use crate::remote::utils::DROP_CLIENT_ASYNC;
|
||||||
|
use crate::remote::utils::DROP_CLIENT_SYNC;
|
||||||
|
use bitcode::{Decode, Encode};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct FetchGameStruct {
|
pub struct FetchGameStruct {
|
||||||
@ -37,25 +33,64 @@ pub struct FetchGameStruct {
|
|||||||
version: Option<GameVersion>,
|
version: Option<GameVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_library_logic(
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
|
||||||
context: &DropRemoteContext,
|
#[serde(rename_all = "camelCase")]
|
||||||
hard_fresh: Option<bool>,
|
pub struct Game {
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
id: String,
|
||||||
let do_hard_refresh = hard_fresh.unwrap_or(false);
|
m_name: String,
|
||||||
if !do_hard_refresh && let Ok(library) = get_cached_object("library") {
|
m_short_description: String,
|
||||||
return Ok(library);
|
m_description: String,
|
||||||
}
|
// mDevelopers
|
||||||
|
// mPublishers
|
||||||
|
m_icon_object_id: String,
|
||||||
|
m_banner_object_id: String,
|
||||||
|
m_cover_object_id: String,
|
||||||
|
m_image_library_object_ids: Vec<String>,
|
||||||
|
m_image_carousel_object_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct GameUpdateEvent {
|
||||||
|
pub game_id: String,
|
||||||
|
pub status: (
|
||||||
|
Option<GameDownloadStatus>,
|
||||||
|
Option<ApplicationTransientStatus>,
|
||||||
|
),
|
||||||
|
pub version: Option<GameVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct QueueUpdateEventQueueData {
|
||||||
|
pub meta: DownloadableMetadata,
|
||||||
|
pub status: DownloadStatus,
|
||||||
|
pub progress: f64,
|
||||||
|
pub current: usize,
|
||||||
|
pub max: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct QueueUpdateEvent {
|
||||||
|
pub queue: Vec<QueueUpdateEventQueueData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct StatsUpdateEvent {
|
||||||
|
pub speed: usize,
|
||||||
|
pub time: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_library_logic(
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
let response = generate_url(context, &["/api/v1/client/user/library"], &[])?;
|
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
|
||||||
let response = client
|
let response = client
|
||||||
.get(response)
|
.get(response)
|
||||||
.header("Authorization", generate_authorization_header(context))
|
.header("Authorization", generate_authorization_header())
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
let err = response.json().await.unwrap_or(ServerError {
|
let err = response.json().await.unwrap_or(DropServerError {
|
||||||
status_code: 500,
|
status_code: 500,
|
||||||
status_message: "Invalid response from server.".to_owned(),
|
status_message: "Invalid response from server.".to_owned(),
|
||||||
});
|
});
|
||||||
@ -65,13 +100,12 @@ pub async fn fetch_library_logic(
|
|||||||
|
|
||||||
let mut games: Vec<Game> = response.json().await?;
|
let mut games: Vec<Game> = response.json().await?;
|
||||||
|
|
||||||
|
let mut handle = state.lock().unwrap();
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
|
||||||
for game in &games {
|
for game in &games {
|
||||||
db_handle
|
handle.games.insert(game.id.clone(), game.clone());
|
||||||
.applications
|
|
||||||
.games
|
|
||||||
.insert(game.id.clone(), game.clone());
|
|
||||||
if !db_handle.applications.game_statuses.contains_key(&game.id) {
|
if !db_handle.applications.game_statuses.contains_key(&game.id) {
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
@ -87,7 +121,7 @@ pub async fn fetch_library_logic(
|
|||||||
}
|
}
|
||||||
// We should always have a cache of the object
|
// We should always have a cache of the object
|
||||||
// Pass db_handle because otherwise we get a gridlock
|
// Pass db_handle because otherwise we get a gridlock
|
||||||
let game = match get_cached_object_db::<Game>(&meta.id.clone()) {
|
let game = match get_cached_object_db::<Game>(&meta.id.clone(), &db_handle) {
|
||||||
Ok(game) => game,
|
Ok(game) => game,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
@ -100,13 +134,14 @@ pub async fn fetch_library_logic(
|
|||||||
games.push(game);
|
games.push(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drop(handle);
|
||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
cache_object("library", &games)?;
|
cache_object("library", &games)?;
|
||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
pub async fn fetch_library_logic_offline(
|
pub async fn fetch_library_logic_offline(
|
||||||
_hard_refresh: Option<bool>,
|
_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")?;
|
||||||
|
|
||||||
@ -126,10 +161,12 @@ pub async fn fetch_library_logic_offline(
|
|||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
pub async fn fetch_game_logic(
|
pub async fn fetch_game_logic(
|
||||||
context: &DropRemoteContext,
|
|
||||||
id: String,
|
id: String,
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
let version = {
|
let version = {
|
||||||
|
let state_handle = state.lock().unwrap();
|
||||||
|
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
|
|
||||||
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
let metadata_option = db_lock.applications.installed_game_version.get(&id);
|
||||||
@ -143,7 +180,7 @@ pub async fn fetch_game_logic(
|
|||||||
.cloned(),
|
.cloned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let game = db_lock.applications.games.get(&id);
|
let game = state_handle.games.get(&id);
|
||||||
if let Some(game) = game {
|
if let Some(game) = game {
|
||||||
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
let status = GameStatusManager::fetch_state(&id, &db_lock);
|
||||||
|
|
||||||
@ -162,15 +199,15 @@ pub async fn fetch_game_logic(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
let response = generate_url(context, &["/api/v1/client/game/", &id], &[])?;
|
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
|
||||||
let response = client
|
let response = client
|
||||||
.get(response)
|
.get(response)
|
||||||
.header("Authorization", generate_authorization_header(context))
|
.header("Authorization", generate_authorization_header())
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if response.status() == 404 {
|
if response.status() == 404 {
|
||||||
let offline_fetch = fetch_game_logic_offline(id.clone()).await;
|
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
|
||||||
if let Ok(fetch_data) = offline_fetch {
|
if let Ok(fetch_data) = offline_fetch {
|
||||||
return Ok(fetch_data);
|
return Ok(fetch_data);
|
||||||
}
|
}
|
||||||
@ -185,11 +222,10 @@ pub async fn fetch_game_logic(
|
|||||||
|
|
||||||
let game: Game = response.json().await?;
|
let game: Game = response.json().await?;
|
||||||
|
|
||||||
|
let mut state_handle = state.lock().unwrap();
|
||||||
|
state_handle.games.insert(id.clone(), game.clone());
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
db_handle
|
|
||||||
.applications
|
|
||||||
.games
|
|
||||||
.insert(id.clone(), game.clone());
|
|
||||||
|
|
||||||
db_handle
|
db_handle
|
||||||
.applications
|
.applications
|
||||||
@ -212,7 +248,10 @@ pub async fn fetch_game_logic(
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_game_logic_offline(id: String) -> Result<FetchGameStruct, RemoteAccessError> {
|
pub async fn fetch_game_logic_offline(
|
||||||
|
id: String,
|
||||||
|
_state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> 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 {
|
||||||
@ -238,19 +277,15 @@ pub async fn fetch_game_logic_offline(id: String) -> Result<FetchGameStruct, Rem
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_game_version_options_logic(
|
pub async fn fetch_game_version_options_logic(
|
||||||
context: &DropRemoteContext,
|
|
||||||
game_id: String,
|
game_id: String,
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
|
|
||||||
let response = generate_url(
|
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
|
||||||
context,
|
|
||||||
&["/api/v1/client/game/versions"],
|
|
||||||
&[("id", &game_id)],
|
|
||||||
)?;
|
|
||||||
let response = client
|
let response = client
|
||||||
.get(response)
|
.get(response)
|
||||||
.header("Authorization", generate_authorization_header(context))
|
.header("Authorization", generate_authorization_header())
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -262,6 +297,19 @@ pub async fn fetch_game_version_options_logic(
|
|||||||
|
|
||||||
let data: Vec<GameVersion> = response.json().await?;
|
let data: Vec<GameVersion> = response.json().await?;
|
||||||
|
|
||||||
|
let state_lock = state.lock().unwrap();
|
||||||
|
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||||
|
let data: Vec<GameVersion> = data
|
||||||
|
.into_iter()
|
||||||
|
.filter(|v| {
|
||||||
|
process_manager_lock
|
||||||
|
.valid_platform(&v.platform, &state_lock)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
drop(process_manager_lock);
|
||||||
|
drop(state_lock);
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +442,6 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_game_complete(
|
pub fn on_game_complete(
|
||||||
context: &DropRemoteContext,
|
|
||||||
meta: &DownloadableMetadata,
|
meta: &DownloadableMetadata,
|
||||||
install_dir: String,
|
install_dir: String,
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
@ -406,7 +453,6 @@ pub fn on_game_complete(
|
|||||||
|
|
||||||
let client = DROP_CLIENT_SYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
let response = generate_url(
|
let response = generate_url(
|
||||||
context,
|
|
||||||
&["/api/v1/client/game/version"],
|
&["/api/v1/client/game/version"],
|
||||||
&[
|
&[
|
||||||
("id", &meta.id),
|
("id", &meta.id),
|
||||||
@ -415,7 +461,7 @@ pub fn on_game_complete(
|
|||||||
)?;
|
)?;
|
||||||
let response = client
|
let response = client
|
||||||
.get(response)
|
.get(response)
|
||||||
.header("Authorization", generate_authorization_header(context))
|
.header("Authorization", generate_authorization_header())
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
let game_version: GameVersion = response.json()?;
|
let game_version: GameVersion = response.json()?;
|
||||||
@ -475,8 +521,7 @@ pub fn push_game_update(
|
|||||||
) {
|
) {
|
||||||
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
|
||||||
&status.0
|
&status.0
|
||||||
&& version.is_none()
|
&& version.is_none() {
|
||||||
{
|
|
||||||
panic!("pushed game for installed game that doesn't have version information");
|
panic!("pushed game for installed game that doesn't have version information");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,3 +536,48 @@ pub fn push_game_update(
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FrontendGameOptions {
|
||||||
|
launch_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_game_configuration(
|
||||||
|
game_id: String,
|
||||||
|
options: FrontendGameOptions,
|
||||||
|
) -> Result<(), LibraryError> {
|
||||||
|
let mut handle = borrow_db_mut_checked();
|
||||||
|
let installed_version = handle
|
||||||
|
.applications
|
||||||
|
.installed_game_version
|
||||||
|
.get(&game_id)
|
||||||
|
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
||||||
|
|
||||||
|
let id = installed_version.id.clone();
|
||||||
|
let version = installed_version.version.clone().unwrap();
|
||||||
|
|
||||||
|
let mut existing_configuration = handle
|
||||||
|
.applications
|
||||||
|
.game_versions
|
||||||
|
.get(&id)
|
||||||
|
.unwrap()
|
||||||
|
.get(&version)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// Add more options in here
|
||||||
|
existing_configuration.launch_command_template = options.launch_string;
|
||||||
|
|
||||||
|
// Add no more options past here
|
||||||
|
|
||||||
|
handle
|
||||||
|
.applications
|
||||||
|
.game_versions
|
||||||
|
.get_mut(&id)
|
||||||
|
.unwrap()
|
||||||
|
.insert(version.to_string(), existing_configuration);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
5
src-tauri/src/games/mod.rs
Normal file
5
src-tauri/src/games/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod collections;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod downloads;
|
||||||
|
pub mod library;
|
||||||
|
pub mod state;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// use drop_database::models::data::{ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus};
|
use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus};
|
||||||
|
|
||||||
pub type GameStatusWithTransient = (
|
pub type GameStatusWithTransient = (
|
||||||
Option<GameDownloadStatus>,
|
Option<GameDownloadStatus>,
|
||||||
@ -8,16 +8,10 @@ pub struct GameStatusManager {}
|
|||||||
|
|
||||||
impl GameStatusManager {
|
impl GameStatusManager {
|
||||||
pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient {
|
pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient {
|
||||||
let online_state = database
|
let online_state = match database.applications.installed_game_version.get(game_id) {
|
||||||
.applications
|
Some(meta) => database.applications.transient_statuses.get(meta).cloned(),
|
||||||
.transient_statuses
|
None => None,
|
||||||
.get(&DownloadableMetadata {
|
};
|
||||||
id: game_id.to_string(),
|
|
||||||
download_type: DownloadType::Game,
|
|
||||||
version: None,
|
|
||||||
})
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
let offline_state = database.applications.game_statuses.get(game_id).cloned();
|
let offline_state = database.applications.game_statuses.get(game_id).cloned();
|
||||||
|
|
||||||
if online_state.is_some() {
|
if online_state.is_some() {
|
||||||
@ -4,60 +4,80 @@
|
|||||||
#![feature(duration_millis_float)]
|
#![feature(duration_millis_float)]
|
||||||
#![feature(iterator_try_collect)]
|
#![feature(iterator_try_collect)]
|
||||||
#![deny(clippy::all)]
|
#![deny(clippy::all)]
|
||||||
|
#![deny(clippy::unwrap_used)]
|
||||||
|
|
||||||
mod auth;
|
|
||||||
mod client;
|
|
||||||
mod database;
|
mod database;
|
||||||
|
mod games;
|
||||||
|
|
||||||
|
mod client;
|
||||||
mod download_manager;
|
mod download_manager;
|
||||||
mod native_library;
|
mod error;
|
||||||
mod process;
|
mod process;
|
||||||
mod remote;
|
mod remote;
|
||||||
mod setup;
|
mod utils;
|
||||||
|
|
||||||
use crate::auth::recieve_handshake;
|
use crate::database::scan::scan_install_dirs;
|
||||||
use crate::native_library::collection_commands::{add_game_to_collection, create_collection, delete_collection, delete_game_in_collection, fetch_collection, fetch_collections};
|
use crate::process::commands::open_process_logs;
|
||||||
use crate::native_library::commands::{
|
use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE;
|
||||||
fetch_game, fetch_game_status, fetch_game_version_options, fetch_library, uninstall_game,
|
|
||||||
};
|
|
||||||
use crate::native_library::downloads::commands::{download_game, resume_download};
|
|
||||||
use crate::process::commands::{open_process_logs, update_game_configuration};
|
|
||||||
use crate::remote::commands::auth_initiate_code;
|
use crate::remote::commands::auth_initiate_code;
|
||||||
use crate::remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
|
||||||
|
use bitcode::{Decode, Encode};
|
||||||
use client::commands::fetch_state;
|
use client::commands::fetch_state;
|
||||||
use client::{
|
use client::{
|
||||||
autostart::{get_autostart_enabled, toggle_autostart},
|
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
||||||
cleanup::{cleanup_and_exit, quit},
|
cleanup::{cleanup_and_exit, quit},
|
||||||
};
|
};
|
||||||
use database::commands::{
|
use database::commands::{
|
||||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
||||||
fetch_system_data, update_settings,
|
fetch_system_data, update_settings,
|
||||||
};
|
};
|
||||||
|
use database::db::{DATA_ROOT_DIR, DatabaseInterface, borrow_db_checked, borrow_db_mut_checked};
|
||||||
|
use database::models::data::GameDownloadStatus;
|
||||||
use download_manager::commands::{
|
use download_manager::commands::{
|
||||||
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
||||||
};
|
};
|
||||||
use drop_database::borrow_db_mut_checked;
|
use download_manager::download_manager_builder::DownloadManagerBuilder;
|
||||||
use drop_database::db::DATA_ROOT_DIR;
|
use download_manager::download_manager_frontend::DownloadManager;
|
||||||
use drop_database::runtime_models::User;
|
use games::collections::commands::{
|
||||||
use drop_downloads::download_manager_frontend::DownloadManager;
|
add_game_to_collection, create_collection, delete_collection, delete_game_in_collection,
|
||||||
use drop_process::process_manager::ProcessManager;
|
fetch_collection, fetch_collections,
|
||||||
use drop_remote::{fetch_object::fetch_object, offline};
|
};
|
||||||
use log::{debug, info, warn};
|
use games::commands::{
|
||||||
|
fetch_game, fetch_game_status, fetch_game_version_options, fetch_library, uninstall_game,
|
||||||
|
};
|
||||||
|
use games::downloads::commands::download_game;
|
||||||
|
use games::library::{Game, update_game_configuration};
|
||||||
|
use log::{LevelFilter, debug, info, warn, error};
|
||||||
|
use log4rs::Config;
|
||||||
|
use log4rs::append::console::ConsoleAppender;
|
||||||
|
use log4rs::append::file::FileAppender;
|
||||||
|
use log4rs::config::{Appender, Root};
|
||||||
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
use process::commands::{kill_game, launch_game};
|
use process::commands::{kill_game, launch_game};
|
||||||
|
use process::process_manager::ProcessManager;
|
||||||
|
use remote::auth::{self, recieve_handshake};
|
||||||
use remote::commands::{
|
use remote::commands::{
|
||||||
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect,
|
auth_initiate, fetch_drop_object, gen_drop_url, manual_recieve_handshake, retry_connect,
|
||||||
sign_out, use_remote,
|
sign_out, use_remote,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use remote::fetch_object::fetch_object;
|
||||||
|
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
||||||
|
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;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
};
|
||||||
use std::{env, panic};
|
use std::{env, panic};
|
||||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
use tauri::{Manager, RunEvent, WindowEvent};
|
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
@ -72,18 +92,165 @@ pub enum AppStatus {
|
|||||||
ServerUnavailable,
|
ServerUnavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct User {
|
||||||
|
id: String,
|
||||||
|
username: String,
|
||||||
|
admin: bool,
|
||||||
|
display_name: String,
|
||||||
|
profile_picture_object_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CompatInfo {
|
||||||
|
umu_installed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_new_compat_info() -> Option<CompatInfo> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return None;
|
||||||
|
|
||||||
|
let has_umu_installed = UMU_LAUNCHER_EXECUTABLE.is_some();
|
||||||
|
Some(CompatInfo {
|
||||||
|
umu_installed: has_umu_installed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppState {
|
pub struct AppState<'a> {
|
||||||
status: AppStatus,
|
status: AppStatus,
|
||||||
user: Option<User>,
|
user: Option<User>,
|
||||||
|
games: HashMap<String, Game>,
|
||||||
|
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
download_manager: Arc<DownloadManager>,
|
download_manager: Arc<DownloadManager>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
process_manager: &'static Mutex<ProcessManager<'static>>,
|
process_manager: Arc<Mutex<ProcessManager<'a>>>,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
compat_info: Option<CompatInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn setup(handle: AppHandle) -> AppState<'static> {
|
||||||
|
let logfile = FileAppender::builder()
|
||||||
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
|
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||||
|
)))
|
||||||
|
.append(false)
|
||||||
|
.build(DATA_ROOT_DIR.join("./drop.log"))
|
||||||
|
.expect("Failed to setup logfile");
|
||||||
|
|
||||||
|
let console = ConsoleAppender::builder()
|
||||||
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
|
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||||
|
)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info"));
|
||||||
|
|
||||||
|
let config = Config::builder()
|
||||||
|
.appenders(vec![
|
||||||
|
Appender::builder().build("logfile", Box::new(logfile)),
|
||||||
|
Appender::builder().build("console", Box::new(console)),
|
||||||
|
])
|
||||||
|
.build(
|
||||||
|
Root::builder()
|
||||||
|
.appenders(vec!["logfile", "console"])
|
||||||
|
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
|
||||||
|
)
|
||||||
|
.expect("Failed to build config");
|
||||||
|
|
||||||
|
log4rs::init_config(config).expect("Failed to initialise log4rs");
|
||||||
|
|
||||||
|
let games = HashMap::new();
|
||||||
|
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
||||||
|
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
|
||||||
|
let compat_info = create_new_compat_info();
|
||||||
|
|
||||||
|
debug!("checking if database is set up");
|
||||||
|
let is_set_up = DB.database_is_set_up();
|
||||||
|
|
||||||
|
scan_install_dirs();
|
||||||
|
|
||||||
|
if !is_set_up {
|
||||||
|
return AppState {
|
||||||
|
status: AppStatus::NotConfigured,
|
||||||
|
user: None,
|
||||||
|
games,
|
||||||
|
download_manager,
|
||||||
|
process_manager,
|
||||||
|
compat_info,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("database is set up");
|
||||||
|
|
||||||
|
// TODO: Account for possible failure
|
||||||
|
let (app_status, user) = auth::setup().await;
|
||||||
|
|
||||||
|
let db_handle = borrow_db_checked();
|
||||||
|
let mut missing_games = Vec::new();
|
||||||
|
let statuses = db_handle.applications.game_statuses.clone();
|
||||||
|
drop(db_handle);
|
||||||
|
|
||||||
|
for (game_id, status) in statuses {
|
||||||
|
match status {
|
||||||
|
GameDownloadStatus::Remote {} => {}
|
||||||
|
GameDownloadStatus::PartiallyInstalled { .. } => {}
|
||||||
|
GameDownloadStatus::SetupRequired {
|
||||||
|
version_name: _,
|
||||||
|
install_dir,
|
||||||
|
} => {
|
||||||
|
let install_dir_path = Path::new(&install_dir);
|
||||||
|
if !install_dir_path.exists() {
|
||||||
|
missing_games.push(game_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameDownloadStatus::Installed {
|
||||||
|
version_name: _,
|
||||||
|
install_dir,
|
||||||
|
} => {
|
||||||
|
let install_dir_path = Path::new(&install_dir);
|
||||||
|
if !install_dir_path.exists() {
|
||||||
|
missing_games.push(game_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("detected games missing: {missing_games:?}");
|
||||||
|
|
||||||
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
|
for game_id in missing_games {
|
||||||
|
db_handle
|
||||||
|
.applications
|
||||||
|
.game_statuses
|
||||||
|
.entry(game_id)
|
||||||
|
.and_modify(|v| *v = GameDownloadStatus::Remote {});
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(db_handle);
|
||||||
|
|
||||||
|
debug!("finished setup!");
|
||||||
|
|
||||||
|
// Sync autostart state
|
||||||
|
if let Err(e) = sync_autostart_on_startup(&handle) {
|
||||||
|
warn!("failed to sync autostart state: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppState {
|
||||||
|
status: app_status,
|
||||||
|
user,
|
||||||
|
games,
|
||||||
|
download_manager,
|
||||||
|
process_manager,
|
||||||
|
compat_info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
|
||||||
|
|
||||||
pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
|
pub fn custom_panic_handler(e: &PanicHookInfo) -> Option<()> {
|
||||||
let crash_file = DATA_ROOT_DIR.join(format!(
|
let crash_file = DATA_ROOT_DIR.join(format!(
|
||||||
"crash-{}.log",
|
"crash-{}.log",
|
||||||
@ -181,7 +348,7 @@ pub fn run() {
|
|||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
||||||
tauri::async_runtime::block_on(async move {
|
tauri::async_runtime::block_on(async move {
|
||||||
let state = setup::setup(handle).await;
|
let state = setup(handle).await;
|
||||||
info!("initialized drop client");
|
info!("initialized drop client");
|
||||||
app.manage(Mutex::new(state));
|
app.manage(Mutex::new(state));
|
||||||
|
|
||||||
@ -205,42 +372,57 @@ pub fn run() {
|
|||||||
.shadow(false)
|
.shadow(false)
|
||||||
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
.data_directory(DATA_ROOT_DIR.join(".webview"))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.expect("Failed to build main window");
|
||||||
|
|
||||||
app.deep_link().on_open_url(move |event| {
|
app.deep_link().on_open_url(move |event| {
|
||||||
debug!("handling drop:// url");
|
debug!("handling drop:// url");
|
||||||
let binding = event.urls();
|
let binding = event.urls();
|
||||||
let url = binding.first().unwrap();
|
let url = match binding.first() {
|
||||||
if url.host_str().unwrap() == "handshake" {
|
Some(url) => url,
|
||||||
|
None => {
|
||||||
|
warn!("No value recieved from deep link. Is this a drop server?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some("handshake") = url.host_str() {
|
||||||
tauri::async_runtime::spawn(recieve_handshake(
|
tauri::async_runtime::spawn(recieve_handshake(
|
||||||
handle.clone(),
|
handle.clone(),
|
||||||
url.path().to_string(),
|
url.path().to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let open_menu_item = MenuItem::with_id(app, "open", "Open", true, None::<&str>).expect("Failed to generate open menu item");
|
||||||
|
|
||||||
|
let sep = PredefinedMenuItem::separator(app).expect("Failed to generate menu separator item");
|
||||||
|
|
||||||
|
let quit_menu_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).expect("Failed to generate quit menu item");
|
||||||
|
|
||||||
let menu = Menu::with_items(
|
let menu = Menu::with_items(
|
||||||
app,
|
app,
|
||||||
&[
|
&[
|
||||||
&MenuItem::with_id(app, "open", "Open", true, None::<&str>).unwrap(),
|
&open_menu_item,
|
||||||
&PredefinedMenuItem::separator(app).unwrap(),
|
&sep,
|
||||||
/*
|
/*
|
||||||
&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(),
|
&quit_menu_item,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.expect("Failed to generate menu");
|
||||||
|
|
||||||
run_on_tray(|| {
|
run_on_tray(|| {
|
||||||
TrayIconBuilder::new()
|
TrayIconBuilder::new()
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().expect("Failed to get default window icon").clone())
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
"open" => {
|
"open" => {
|
||||||
app.webview_windows().get("main").unwrap().show().unwrap();
|
app.webview_windows()
|
||||||
|
.get("main")
|
||||||
|
.expect("Failed to get webview")
|
||||||
|
.show()
|
||||||
|
.expect("Failed to show window");
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
cleanup_and_exit(app, &app.state());
|
cleanup_and_exit(app, &app.state());
|
||||||
@ -257,15 +439,19 @@ pub fn run() {
|
|||||||
{
|
{
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
if let Some(original) = db_handle.prev_database.take() {
|
if let Some(original) = db_handle.prev_database.take() {
|
||||||
|
let canonicalised = match original.canonicalize() {
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(_) => original,
|
||||||
|
};
|
||||||
warn!(
|
warn!(
|
||||||
"Database corrupted. Original file at {}",
|
"Database corrupted. Original file at {}",
|
||||||
original.canonicalize().unwrap().to_string_lossy()
|
canonicalised.display()
|
||||||
);
|
);
|
||||||
app.dialog()
|
app.dialog()
|
||||||
.message(
|
.message(format!(
|
||||||
"Database corrupted. A copy has been saved at: ".to_string()
|
"Database corrupted. A copy has been saved at: {}",
|
||||||
+ original.to_str().unwrap(),
|
canonicalised.display()
|
||||||
)
|
))
|
||||||
.title("Database corrupted")
|
.title("Database corrupted")
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
}
|
}
|
||||||
@ -298,7 +484,7 @@ pub fn run() {
|
|||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
run_on_tray(|| {
|
run_on_tray(|| {
|
||||||
window.hide().unwrap();
|
window.hide().expect("Failed to close window in tray");
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, models::data::GameVersion, runtime_models::Game};
|
|
||||||
use drop_errors::{library_error::LibraryError, remote_access_error::RemoteAccessError};
|
|
||||||
use drop_native_library::{library::{fetch_game_logic, fetch_game_logic_offline, fetch_game_version_options_logic, fetch_library_logic, fetch_library_logic_offline, get_current_meta, uninstall_game_logic, FetchGameStruct}, state::{GameStatusManager, GameStatusWithTransient}};
|
|
||||||
use tauri::AppHandle;
|
|
||||||
|
|
||||||
use crate::{AppState, offline};
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn fetch_library(
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
hard_refresh: Option<bool>,
|
|
||||||
) -> Result<Vec<Game>, RemoteAccessError> {
|
|
||||||
offline!(
|
|
||||||
state,
|
|
||||||
fetch_library_logic,
|
|
||||||
fetch_library_logic_offline,
|
|
||||||
hard_refresh
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn fetch_game(
|
|
||||||
game_id: String,
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
|
||||||
offline!(
|
|
||||||
state,
|
|
||||||
fetch_game_logic,
|
|
||||||
fetch_game_logic_offline,
|
|
||||||
game_id
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
GameStatusManager::fetch_state(&id, &db_handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
|
|
||||||
let meta = match get_current_meta(&game_id) {
|
|
||||||
Some(data) => data,
|
|
||||||
None => return Err(LibraryError::MetaNotFound(game_id)),
|
|
||||||
};
|
|
||||||
uninstall_game_logic(meta, &app_handle);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn fetch_game_version_options(
|
|
||||||
game_id: String,
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
) -> Result<Vec<GameVersion>, RemoteAccessError> {
|
|
||||||
let all_versions = fetch_game_version_options_logic(game_id).await?;
|
|
||||||
|
|
||||||
let state_lock = state.lock().unwrap();
|
|
||||||
let process_manager_lock = state_lock.process_manager.lock().unwrap();
|
|
||||||
let data: Vec<GameVersion> = all_versions
|
|
||||||
.into_iter()
|
|
||||||
.filter(|v| {
|
|
||||||
process_manager_lock
|
|
||||||
.valid_platform(&v.platform)
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
drop(process_manager_lock);
|
|
||||||
drop(state_lock);
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod collection_commands;
|
|
||||||
pub mod commands;
|
|
||||||
pub mod downloads;
|
|
||||||
@ -1,10 +1,6 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use drop_database::borrow_db_mut_checked;
|
use crate::{error::process_error::ProcessError, AppState};
|
||||||
use drop_errors::{library_error::LibraryError, process_error::ProcessError};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn launch_game(
|
pub fn launch_game(
|
||||||
@ -20,7 +16,7 @@ pub fn launch_game(
|
|||||||
// download_type: DownloadType::Game,
|
// download_type: DownloadType::Game,
|
||||||
//};
|
//};
|
||||||
|
|
||||||
match process_manager_lock.launch_process(id, state_lock.process_manager) {
|
match process_manager_lock.launch_process(id, &state_lock) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
@ -52,48 +48,3 @@ pub fn open_process_logs(
|
|||||||
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
|
||||||
process_manager_lock.open_process_logs(game_id)
|
process_manager_lock.open_process_logs(game_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct FrontendGameOptions {
|
|
||||||
pub launch_string: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn update_game_configuration(
|
|
||||||
game_id: String,
|
|
||||||
options: FrontendGameOptions,
|
|
||||||
) -> Result<(), LibraryError> {
|
|
||||||
let mut handle = borrow_db_mut_checked();
|
|
||||||
let installed_version = handle
|
|
||||||
.applications
|
|
||||||
.installed_game_version
|
|
||||||
.get(&game_id)
|
|
||||||
.ok_or(LibraryError::MetaNotFound(game_id))?;
|
|
||||||
|
|
||||||
let id = installed_version.id.clone();
|
|
||||||
let version = installed_version.version.clone().unwrap();
|
|
||||||
|
|
||||||
let mut existing_configuration = handle
|
|
||||||
.applications
|
|
||||||
.game_versions
|
|
||||||
.get(&id)
|
|
||||||
.unwrap()
|
|
||||||
.get(&version)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
// Add more options in here
|
|
||||||
existing_configuration.launch_command_template = options.launch_string;
|
|
||||||
|
|
||||||
// Add no more options past here
|
|
||||||
|
|
||||||
handle
|
|
||||||
.applications
|
|
||||||
.game_versions
|
|
||||||
.get_mut(&id)
|
|
||||||
.unwrap()
|
|
||||||
.insert(version.to_string(), existing_configuration);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod process_manager;
|
||||||
|
pub mod process_handlers;
|
||||||
|
pub mod format;
|
||||||
|
pub mod utils;
|
||||||
@ -5,11 +5,13 @@ use std::{
|
|||||||
sync::LazyLock,
|
sync::LazyLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::{models::data::{Database, DownloadableMetadata, GameVersion}, process::Platform};
|
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
|
||||||
use crate::process_manager::ProcessHandler;
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
database::models::data::{Database, DownloadableMetadata, GameVersion},
|
||||||
|
process::process_manager::{Platform, ProcessHandler},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct NativeGameLauncher;
|
pub struct NativeGameLauncher;
|
||||||
impl ProcessHandler for NativeGameLauncher {
|
impl ProcessHandler for NativeGameLauncher {
|
||||||
@ -24,7 +26,7 @@ impl ProcessHandler for NativeGameLauncher {
|
|||||||
format!("\"{}\" {}", launch_command, args.join(" "))
|
format!("\"{}\" {}", launch_command, args.join(" "))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,8 +85,11 @@ impl ProcessHandler for UMULauncher {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
||||||
UMU_LAUNCHER_EXECUTABLE.is_some()
|
let Some(ref compat_info) = state.compat_info else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
compat_info.umu_installed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +123,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
|||||||
|
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
|
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@ -130,6 +135,10 @@ impl ProcessHandler for AsahiMuvmLauncher {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UMU_LAUNCHER_EXECUTABLE.is_some()
|
let Some(ref compat_info) = state.compat_info else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
compat_info.umu_installed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,17 +10,30 @@ use std::{
|
|||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, models::data::{ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus, GameVersion}, process::Platform, DB};
|
|
||||||
use drop_errors::process_error::ProcessError;
|
|
||||||
use drop_native_library::{library::push_game_update, state::GameStatusManager};
|
|
||||||
use dynfmt::Format;
|
use dynfmt::Format;
|
||||||
use dynfmt::SimpleCurlyFormat;
|
use dynfmt::SimpleCurlyFormat;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use shared_child::SharedChild;
|
use shared_child::SharedChild;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
use crate::{format::DropFormatArgs, process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher}};
|
use crate::{
|
||||||
|
AppState, DB,
|
||||||
|
database::{
|
||||||
|
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
|
||||||
|
models::data::{
|
||||||
|
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
|
||||||
|
GameDownloadStatus, GameVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error::process_error::ProcessError,
|
||||||
|
games::{library::push_game_update, state::GameStatusManager},
|
||||||
|
process::{
|
||||||
|
format::DropFormatArgs,
|
||||||
|
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct RunningProcess {
|
pub struct RunningProcess {
|
||||||
handle: Arc<SharedChild>,
|
handle: Arc<SharedChild>,
|
||||||
@ -181,6 +194,7 @@ impl ProcessManager<'_> {
|
|||||||
fn fetch_process_handler(
|
fn fetch_process_handler(
|
||||||
&self,
|
&self,
|
||||||
db_lock: &Database,
|
db_lock: &Database,
|
||||||
|
state: &AppState,
|
||||||
target_platform: &Platform,
|
target_platform: &Platform,
|
||||||
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
|
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
|
||||||
Ok(self
|
Ok(self
|
||||||
@ -190,22 +204,22 @@ impl ProcessManager<'_> {
|
|||||||
let (e_current, e_target) = e.0;
|
let (e_current, e_target) = e.0;
|
||||||
e_current == self.current_platform
|
e_current == self.current_platform
|
||||||
&& e_target == *target_platform
|
&& e_target == *target_platform
|
||||||
&& e.1.valid_for_platform(db_lock, target_platform)
|
&& e.1.valid_for_platform(db_lock, state, target_platform)
|
||||||
})
|
})
|
||||||
.ok_or(ProcessError::InvalidPlatform)?
|
.ok_or(ProcessError::InvalidPlatform)?
|
||||||
.1)
|
.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid_platform(&self, platform: &Platform,) -> Result<bool, String> {
|
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
|
||||||
let db_lock = borrow_db_checked();
|
let db_lock = borrow_db_checked();
|
||||||
let process_handler = self.fetch_process_handler(&db_lock, platform);
|
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
|
||||||
Ok(process_handler.is_ok())
|
Ok(process_handler.is_ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch_process(
|
pub fn launch_process(
|
||||||
&mut self,
|
&mut self,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
process_manager_lock: &'static Mutex<ProcessManager<'static>>,
|
state: &AppState,
|
||||||
) -> Result<(), ProcessError> {
|
) -> Result<(), ProcessError> {
|
||||||
if self.processes.contains_key(&game_id) {
|
if self.processes.contains_key(&game_id) {
|
||||||
return Err(ProcessError::AlreadyRunning);
|
return Err(ProcessError::AlreadyRunning);
|
||||||
@ -290,7 +304,7 @@ impl ProcessManager<'_> {
|
|||||||
|
|
||||||
let target_platform = game_version.platform;
|
let target_platform = game_version.platform;
|
||||||
|
|
||||||
let process_handler = self.fetch_process_handler(&db_lock, &target_platform)?;
|
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
|
||||||
|
|
||||||
let (launch, args) = match game_status {
|
let (launch, args) = match game_status {
|
||||||
GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
@ -371,17 +385,22 @@ impl ProcessManager<'_> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let wait_thread_handle = launch_process_handle.clone();
|
let wait_thread_handle = launch_process_handle.clone();
|
||||||
|
let wait_thread_apphandle = self.app_handle.clone();
|
||||||
let wait_thread_game_id = meta.clone();
|
let wait_thread_game_id = meta.clone();
|
||||||
|
|
||||||
spawn(move || {
|
spawn(move || {
|
||||||
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
|
||||||
|
|
||||||
let mut process_manager_handle = process_manager_lock.lock().unwrap();
|
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
|
||||||
|
let app_state_handle = app_state.lock().unwrap();
|
||||||
|
|
||||||
|
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
|
||||||
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
|
||||||
|
|
||||||
// As everything goes out of scope, they should get dropped
|
// As everything goes out of scope, they should get dropped
|
||||||
// But just to explicit about it
|
// But just to explicit about it
|
||||||
drop(process_manager_handle);
|
drop(process_manager_handle);
|
||||||
|
drop(app_state_handle);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.processes.insert(
|
self.processes.insert(
|
||||||
@ -396,6 +415,51 @@ impl ProcessManager<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Platform {
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
MacOs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub const HOST: Platform = Self::Windows;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub const HOST: Platform = Self::MacOs;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub const HOST: Platform = Self::Linux;
|
||||||
|
|
||||||
|
pub fn is_case_sensitive(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Windows | Self::MacOs => false,
|
||||||
|
Self::Linux => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Platform {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
match value.to_lowercase().trim() {
|
||||||
|
"windows" => Self::Windows,
|
||||||
|
"linux" => Self::Linux,
|
||||||
|
"mac" | "macos" => Self::MacOs,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<whoami::Platform> for Platform {
|
||||||
|
fn from(value: whoami::Platform) -> Self {
|
||||||
|
match value {
|
||||||
|
whoami::Platform::Windows => Platform::Windows,
|
||||||
|
whoami::Platform::Linux => Platform::Linux,
|
||||||
|
whoami::Platform::MacOS => Platform::MacOs,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ProcessHandler: Send + 'static {
|
pub trait ProcessHandler: Send + 'static {
|
||||||
fn create_launch_process(
|
fn create_launch_process(
|
||||||
&self,
|
&self,
|
||||||
@ -406,5 +470,5 @@ pub trait ProcessHandler: Send + 'static {
|
|||||||
current_dir: &str,
|
current_dir: &str,
|
||||||
) -> String;
|
) -> String;
|
||||||
|
|
||||||
fn valid_for_platform(&self, db: &Database, target: &Platform) -> bool;
|
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
|
||||||
}
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
use std::{io, path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use drop_errors::application_download_error::ApplicationDownloadError;
|
use futures_lite::io;
|
||||||
use sysinfo::{Disk, DiskRefreshKind, Disks};
|
use sysinfo::{Disk, DiskRefreshKind, Disks};
|
||||||
|
|
||||||
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
|
|
||||||
pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownloadError> {
|
pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownloadError> {
|
||||||
let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::nothing().with_storage());
|
let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::nothing().with_storage());
|
||||||
|
|
||||||
221
src-tauri/src/remote/auth.rs
Normal file
221
src-tauri/src/remote/auth.rs
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
use std::{collections::HashMap, env, sync::Mutex};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use droplet_rs::ssl::sign_nonce;
|
||||||
|
use gethostname::gethostname;
|
||||||
|
use log::{debug, error, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{
|
||||||
|
db::{borrow_db_checked, borrow_db_mut_checked},
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
cache::{cache_object, get_cached_object},
|
||||||
|
requests::generate_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CapabilityConfiguration {}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct InitiateRequestBody {
|
||||||
|
name: String,
|
||||||
|
platform: String,
|
||||||
|
capabilities: HashMap<String, CapabilityConfiguration>,
|
||||||
|
mode: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct HandshakeRequestBody {
|
||||||
|
client_id: String,
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct HandshakeResponse {
|
||||||
|
private: String,
|
||||||
|
certificate: String,
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_authorization_header() -> String {
|
||||||
|
let certs = {
|
||||||
|
let db = borrow_db_checked();
|
||||||
|
db.auth.clone().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = Utc::now().timestamp_millis().to_string();
|
||||||
|
|
||||||
|
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
||||||
|
|
||||||
|
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||||
|
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
|
||||||
|
if response.status() != 200 {
|
||||||
|
let err: DropServerError = response.json().await?;
|
||||||
|
warn!("{err:?}");
|
||||||
|
|
||||||
|
if err.status_message == "Nonce expired" {
|
||||||
|
return Err(RemoteAccessError::OutOfSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(RemoteAccessError::InvalidResponse(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
.json::<User>()
|
||||||
|
.await
|
||||||
|
.map_err(std::convert::Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||||
|
let path_chunks: Vec<&str> = path.split('/').collect();
|
||||||
|
if path_chunks.len() != 3 {
|
||||||
|
app.emit("auth/failed", ()).unwrap();
|
||||||
|
return Err(RemoteAccessError::HandshakeFailed(
|
||||||
|
"failed to parse token".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_url = {
|
||||||
|
let handle = borrow_db_checked();
|
||||||
|
Url::parse(handle.base_url.as_str())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let client_id = path_chunks.get(1).unwrap();
|
||||||
|
let token = path_chunks.get(2).unwrap();
|
||||||
|
let body = HandshakeRequestBody {
|
||||||
|
client_id: (*client_id).to_string(),
|
||||||
|
token: (*token).to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||||
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
|
let response = client.post(endpoint).json(&body).send().await?;
|
||||||
|
debug!("handshake responsded with {}", response.status().as_u16());
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
||||||
|
}
|
||||||
|
let response_struct: HandshakeResponse = response.json().await?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut handle = borrow_db_mut_checked();
|
||||||
|
handle.auth = Some(DatabaseAuth {
|
||||||
|
private: response_struct.private,
|
||||||
|
cert: response_struct.certificate,
|
||||||
|
client_id: response_struct.id,
|
||||||
|
web_token: None, // gets created later
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let web_token = {
|
||||||
|
let header = generate_authorization_header();
|
||||||
|
let token = client
|
||||||
|
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
||||||
|
.header("Authorization", header)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
token.text().await.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut handle = borrow_db_mut_checked();
|
||||||
|
let mut_auth = handle.auth.as_mut().unwrap();
|
||||||
|
mut_auth.web_token = Some(web_token);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recieve_handshake(app: AppHandle, path: String) {
|
||||||
|
// Tell the app we're processing
|
||||||
|
app.emit("auth/processing", ()).unwrap();
|
||||||
|
|
||||||
|
let handshake_result = recieve_handshake_logic(&app, path).await;
|
||||||
|
if let Err(e) = handshake_result {
|
||||||
|
warn!("error with authentication: {e}");
|
||||||
|
app.emit("auth/failed", e.to_string()).unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_state = app.state::<Mutex<AppState>>();
|
||||||
|
|
||||||
|
let (app_status, user) = setup().await;
|
||||||
|
|
||||||
|
let mut state_lock = app_state.lock().unwrap();
|
||||||
|
|
||||||
|
state_lock.status = app_status;
|
||||||
|
state_lock.user = user;
|
||||||
|
|
||||||
|
drop(state_lock);
|
||||||
|
|
||||||
|
app.emit("auth/finished", ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
|
||||||
|
let base_url = {
|
||||||
|
let db_lock = borrow_db_checked();
|
||||||
|
Url::parse(&db_lock.base_url.clone())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let hostname = gethostname();
|
||||||
|
|
||||||
|
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
||||||
|
let body = InitiateRequestBody {
|
||||||
|
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
|
||||||
|
platform: env::consts::OS.to_string(),
|
||||||
|
capabilities: HashMap::from([
|
||||||
|
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||||
|
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
||||||
|
]),
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
|
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||||
|
|
||||||
|
if response.status() != 200 {
|
||||||
|
let data: DropServerError = response.json()?;
|
||||||
|
error!("could not start handshake: {}", data.status_message);
|
||||||
|
|
||||||
|
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = response.text()?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup() -> (AppStatus, Option<User>) {
|
||||||
|
let auth = {
|
||||||
|
let data = borrow_db_checked();
|
||||||
|
data.auth.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if auth.is_some() {
|
||||||
|
let user_result = match fetch_user().await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(RemoteAccessError::FetchError(_)) => {
|
||||||
|
let user = get_cached_object::<User>("user").unwrap();
|
||||||
|
return (AppStatus::Offline, Some(user));
|
||||||
|
}
|
||||||
|
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
|
||||||
|
};
|
||||||
|
cache_object("user", &user_result).unwrap();
|
||||||
|
return (AppStatus::SignedIn, Some(user_result));
|
||||||
|
}
|
||||||
|
|
||||||
|
(AppStatus::SignedOut, None)
|
||||||
|
}
|
||||||
@ -5,18 +5,18 @@ use std::{
|
|||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{db::borrow_db_checked, models::data::Database},
|
||||||
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
};
|
||||||
use bitcode::{Decode, DecodeOwned, Encode};
|
use bitcode::{Decode, DecodeOwned, Encode};
|
||||||
use drop_consts::CACHE_DIR;
|
|
||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
|
||||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! offline {
|
macro_rules! offline {
|
||||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||||
|
|
||||||
// TODO add offline mode back
|
async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
|
||||||
// || $var.lock().unwrap().status == AppStatus::Offline
|
|
||||||
async move { if drop_database::borrow_db_checked().settings.force_offline {
|
|
||||||
$func2( $( $arg ), *).await
|
$func2( $( $arg ), *).await
|
||||||
} else {
|
} else {
|
||||||
$func1( $( $arg ), *).await
|
$func1( $( $arg ), *).await
|
||||||
@ -50,43 +50,29 @@ fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
|
|||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_sync(base: &Path, key: &str) -> io::Result<()> {
|
|
||||||
let cache_path = get_cache_path(base, key);
|
|
||||||
std::fs::remove_file(cache_path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
|
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
|
||||||
cache_object_db(key, data)
|
cache_object_db(key, data, &borrow_db_checked())
|
||||||
}
|
}
|
||||||
pub fn cache_object_db<D: Encode>(
|
pub fn cache_object_db<D: Encode>(
|
||||||
key: &str,
|
key: &str,
|
||||||
data: &D,
|
data: &D,
|
||||||
|
database: &Database,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
let bytes = bitcode::encode(data);
|
let bytes = bitcode::encode(data);
|
||||||
write_sync(&CACHE_DIR, key, bytes).map_err(RemoteAccessError::Cache)
|
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
|
||||||
}
|
}
|
||||||
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
|
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
|
||||||
get_cached_object_db::<D>(key)
|
get_cached_object_db::<D>(key, &borrow_db_checked())
|
||||||
}
|
}
|
||||||
pub fn get_cached_object_db<D: DecodeOwned>(
|
pub fn get_cached_object_db<D: DecodeOwned>(
|
||||||
key: &str,
|
key: &str,
|
||||||
|
db: &Database,
|
||||||
) -> Result<D, RemoteAccessError> {
|
) -> Result<D, RemoteAccessError> {
|
||||||
let bytes = read_sync(&CACHE_DIR, key).map_err(RemoteAccessError::Cache)?;
|
let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
|
||||||
let data =
|
let data =
|
||||||
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
|
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
pub fn clear_cached_object(key: &str) -> Result<(), RemoteAccessError> {
|
|
||||||
clear_cached_object_db(key)
|
|
||||||
}
|
|
||||||
pub fn clear_cached_object_db(
|
|
||||||
key: &str,
|
|
||||||
) -> Result<(), RemoteAccessError> {
|
|
||||||
delete_sync(&CACHE_DIR, key).map_err(RemoteAccessError::Cache)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Encode, Decode)]
|
#[derive(Encode, Decode)]
|
||||||
pub struct ObjectCache {
|
pub struct ObjectCache {
|
||||||
content_type: String,
|
content_type: String,
|
||||||
@ -1,8 +1,5 @@
|
|||||||
use std::{sync::Mutex, time::Duration};
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked};
|
|
||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
|
||||||
use drop_remote::{auth::{auth_initiate_logic, generate_authorization_header}, cache::{cache_object, get_cached_object}, requests::generate_url, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT}};
|
|
||||||
use futures_lite::StreamExt;
|
use futures_lite::StreamExt;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use reqwest_websocket::{Message, RequestBuilderExt};
|
use reqwest_websocket::{Message, RequestBuilderExt};
|
||||||
@ -10,12 +7,27 @@ use serde::Deserialize;
|
|||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{auth::{recieve_handshake, setup}, AppState, AppStatus};
|
use crate::{
|
||||||
|
AppState, AppStatus,
|
||||||
|
database::db::{borrow_db_checked, borrow_db_mut_checked},
|
||||||
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
remote::{
|
||||||
|
auth::generate_authorization_header,
|
||||||
|
requests::generate_url,
|
||||||
|
utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
auth::{auth_initiate_logic, recieve_handshake, setup},
|
||||||
|
cache::{cache_object, get_cached_object},
|
||||||
|
utils::use_remote_logic,
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn use_remote(
|
pub async fn use_remote(
|
||||||
url: String,
|
url: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
) -> Result<(), RemoteAccessError> {
|
) -> Result<(), RemoteAccessError> {
|
||||||
use_remote_logic(url, state).await
|
use_remote_logic(url, state).await
|
||||||
}
|
}
|
||||||
@ -75,7 +87,7 @@ pub fn sign_out(app: AppHandle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> Result<(), ()> {
|
pub async fn retry_connect(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<(), ()> {
|
||||||
let (app_status, user) = setup().await;
|
let (app_status, user) = setup().await;
|
||||||
|
|
||||||
let mut guard = state.lock().unwrap();
|
let mut guard = state.lock().unwrap();
|
||||||
@ -139,9 +151,7 @@ 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)
|
manual_recieve_handshake(recieve_app, response.value).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
|
||||||
@ -170,42 +180,3 @@ pub async fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct DropHealthcheck {
|
|
||||||
app_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_remote_logic(
|
|
||||||
url: String,
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
) -> Result<(), RemoteAccessError> {
|
|
||||||
debug!("connecting to url {url}");
|
|
||||||
let base_url = Url::parse(&url)?;
|
|
||||||
|
|
||||||
// Test Drop url
|
|
||||||
let test_endpoint = base_url.join("/api/v1")?;
|
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
|
||||||
let response = client
|
|
||||||
.get(test_endpoint.to_string())
|
|
||||||
.timeout(Duration::from_secs(3))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let result: DropHealthcheck = response.json().await?;
|
|
||||||
|
|
||||||
if result.app_name != "Drop" {
|
|
||||||
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
|
||||||
return Err(RemoteAccessError::InvalidEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut app_state = state.lock().unwrap();
|
|
||||||
app_state.status = AppStatus::SignedOut;
|
|
||||||
drop(app_state);
|
|
||||||
|
|
||||||
let mut db_state = borrow_db_mut_checked();
|
|
||||||
db_state.base_url = base_url.to_string();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Request};
|
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
|
use crate::{database::db::DatabaseImpls, remote::utils::DROP_CLIENT_ASYNC, DB};
|
||||||
use crate::{requests::generate_url, utils::DROP_CLIENT_ASYNC, DropRemoteContext};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::generate_authorization_header,
|
auth::generate_authorization_header,
|
||||||
cache::{ObjectCache, cache_object, get_cached_object},
|
cache::{ObjectCache, cache_object, get_cached_object},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn fetch_object(context: &DropRemoteContext, request: Request<Vec<u8>>, responder: UriSchemeResponder) {
|
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
// Drop leading /
|
// Drop leading /
|
||||||
let object_id = &request.uri().path()[1..];
|
let object_id = &request.uri().path()[1..];
|
||||||
|
|
||||||
@ -22,9 +21,9 @@ pub async fn fetch_object(context: &DropRemoteContext, request: Request<Vec<u8>>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let header = generate_authorization_header(context);
|
let header = generate_authorization_header();
|
||||||
let client = DROP_CLIENT_ASYNC.clone();
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
let url = generate_url(context, &["/api/v1/client/object", object_id], &[]).expect("failed to generated object url");
|
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
|
||||||
let response = client.get(url).header("Authorization", header).send().await;
|
let response = client.get(url).header("Authorization", header).send().await;
|
||||||
|
|
||||||
if response.is_err() {
|
if response.is_err() {
|
||||||
@ -1,2 +1,8 @@
|
|||||||
|
pub mod auth;
|
||||||
|
#[macro_use]
|
||||||
|
pub mod cache;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod fetch_object;
|
||||||
|
pub mod requests;
|
||||||
pub mod server_proto;
|
pub mod server_proto;
|
||||||
|
pub mod utils;
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
use drop_errors::remote_access_error::RemoteAccessError;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{auth::generate_authorization_header, utils::DROP_CLIENT_ASYNC, DropRemoteContext};
|
use crate::{
|
||||||
|
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 generate_url<T: AsRef<str>>(
|
||||||
context: &DropRemoteContext,
|
|
||||||
path_components: &[T],
|
path_components: &[T],
|
||||||
query: &[(T, T)],
|
query: &[(T, T)],
|
||||||
) -> Result<Url, RemoteAccessError> {
|
) -> Result<Url, RemoteAccessError> {
|
||||||
let mut base_url = context.base_url.clone();
|
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,10 +24,10 @@ pub fn generate_url<T: AsRef<str>>(
|
|||||||
Ok(base_url)
|
Ok(base_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn make_authenticated_get(context: &DropRemoteContext, url: Url) -> Result<reqwest::Response, reqwest::Error> {
|
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, reqwest::Error> {
|
||||||
DROP_CLIENT_ASYNC
|
DROP_CLIENT_ASYNC
|
||||||
.get(url)
|
.get(url)
|
||||||
.header("Authorization", generate_authorization_header(context))
|
.header("Authorization", generate_authorization_header())
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use drop_database::borrow_db_checked;
|
|
||||||
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
|
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 async 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)
|
||||||
@ -36,7 +37,7 @@ pub async fn handle_server_proto(request: Request<Vec<u8>>, responder: UriScheme
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = drop_remote::utils::DROP_CLIENT_SYNC.clone();
|
let client = DROP_CLIENT_SYNC.clone();
|
||||||
let response = client
|
let response = client
|
||||||
.request(request.method().clone(), new_uri.to_string())
|
.request(request.method().clone(), new_uri.to_string())
|
||||||
.header("Authorization", format!("Bearer {web_token}"))
|
.header("Authorization", format!("Bearer {web_token}"))
|
||||||
|
|||||||
147
src-tauri/src/remote/utils.rs
Normal file
147
src-tauri/src/remote/utils.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::Read,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use reqwest::Certificate;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppState, AppStatus,
|
||||||
|
database::db::{DATA_ROOT_DIR, borrow_db_mut_checked},
|
||||||
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
state_lock,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct DropHealthcheck {
|
||||||
|
app_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
|
||||||
|
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
|
||||||
|
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
|
||||||
|
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
|
||||||
|
|
||||||
|
fn fetch_certificates() -> Vec<Certificate> {
|
||||||
|
let certificate_dir = DATA_ROOT_DIR.join("certificates");
|
||||||
|
|
||||||
|
let mut certs = Vec::new();
|
||||||
|
match fs::read_dir(certificate_dir) {
|
||||||
|
Ok(c) => {
|
||||||
|
for entry in c {
|
||||||
|
match entry {
|
||||||
|
Ok(c) => {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
match File::open(c.path()) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to open file at {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.read_to_end(&mut buf)
|
||||||
|
.expect(&format!(
|
||||||
|
"Failed to read to end of certificate file {}",
|
||||||
|
c.path().display()
|
||||||
|
));
|
||||||
|
|
||||||
|
match Certificate::from_pem_bundle(&buf) {
|
||||||
|
Ok(certificates) => {
|
||||||
|
for cert in certificates {
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"added {} certificate(s) from {}",
|
||||||
|
certs.len(),
|
||||||
|
c.file_name().display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => warn!(
|
||||||
|
"Invalid certificate file {} with error {}",
|
||||||
|
c.path().display(),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("not loading certificates due to error: {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
certs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_sync() -> reqwest::blocking::Client {
|
||||||
|
let mut client = reqwest::blocking::ClientBuilder::new();
|
||||||
|
|
||||||
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
|
client = client.add_root_certificate(cert.clone());
|
||||||
|
}
|
||||||
|
client.use_rustls_tls().build().expect("Failed to build synchronous client")
|
||||||
|
}
|
||||||
|
pub fn get_client_async() -> reqwest::Client {
|
||||||
|
let mut client = reqwest::ClientBuilder::new();
|
||||||
|
|
||||||
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
|
client = client.add_root_certificate(cert.clone());
|
||||||
|
}
|
||||||
|
client.use_rustls_tls().build().expect("Failed to build asynchronous client")
|
||||||
|
}
|
||||||
|
pub fn get_client_ws() -> reqwest::Client {
|
||||||
|
let mut client = reqwest::ClientBuilder::new();
|
||||||
|
|
||||||
|
for cert in DROP_CERT_BUNDLE.iter() {
|
||||||
|
client = client.add_root_certificate(cert.clone());
|
||||||
|
}
|
||||||
|
client
|
||||||
|
.use_rustls_tls()
|
||||||
|
.http1_only()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build websocket client")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn use_remote_logic(
|
||||||
|
url: String,
|
||||||
|
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||||
|
) -> Result<(), RemoteAccessError> {
|
||||||
|
debug!("connecting to url {url}");
|
||||||
|
let base_url = Url::parse(&url)?;
|
||||||
|
|
||||||
|
// Test Drop url
|
||||||
|
let test_endpoint = base_url.join("/api/v1")?;
|
||||||
|
let client = DROP_CLIENT_ASYNC.clone();
|
||||||
|
let response = client
|
||||||
|
.get(test_endpoint.to_string())
|
||||||
|
.timeout(Duration::from_secs(3))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result: DropHealthcheck = response.json().await?;
|
||||||
|
|
||||||
|
if result.app_name != "Drop" {
|
||||||
|
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
||||||
|
return Err(RemoteAccessError::InvalidEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut app_state = state_lock!(state);
|
||||||
|
app_state.status = AppStatus::SignedOut;
|
||||||
|
drop(app_state);
|
||||||
|
|
||||||
|
let mut db_state = borrow_db_mut_checked();
|
||||||
|
db_state.base_url = base_url.to_string();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,121 +0,0 @@
|
|||||||
use std::{env, path::Path, str::FromStr as _, sync::{Arc, Mutex}};
|
|
||||||
|
|
||||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked, db::{DatabaseImpls as _, DATA_ROOT_DIR}, models::data::GameDownloadStatus, DB};
|
|
||||||
use drop_downloads::download_manager_builder::DownloadManagerBuilder;
|
|
||||||
use drop_process::process_manager::ProcessManager;
|
|
||||||
use log::{debug, info, warn, LevelFilter};
|
|
||||||
use log4rs::{append::{console::ConsoleAppender, file::FileAppender}, config::{Appender, Root}, encode::pattern::PatternEncoder, Config};
|
|
||||||
use tauri::AppHandle;
|
|
||||||
|
|
||||||
use crate::{auth, client::autostart::sync_autostart_on_startup, database::scan::scan_install_dirs, AppState, AppStatus};
|
|
||||||
|
|
||||||
pub async fn setup(handle: AppHandle) -> AppState {
|
|
||||||
let logfile = FileAppender::builder()
|
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
|
||||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
|
||||||
)))
|
|
||||||
.append(false)
|
|
||||||
.build(DATA_ROOT_DIR.join("./drop.log"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let console = ConsoleAppender::builder()
|
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
|
||||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
|
||||||
)))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info"));
|
|
||||||
|
|
||||||
let config = Config::builder()
|
|
||||||
.appenders(vec![
|
|
||||||
Appender::builder().build("logfile", Box::new(logfile)),
|
|
||||||
Appender::builder().build("console", Box::new(console)),
|
|
||||||
])
|
|
||||||
.build(
|
|
||||||
Root::builder()
|
|
||||||
.appenders(vec!["logfile", "console"])
|
|
||||||
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
log4rs::init_config(config).unwrap();
|
|
||||||
|
|
||||||
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
|
|
||||||
let process_manager = Box::leak(Box::new(Mutex::new(ProcessManager::new(handle.clone()))));
|
|
||||||
|
|
||||||
debug!("checking if database is set up");
|
|
||||||
let is_set_up = DB.database_is_set_up();
|
|
||||||
|
|
||||||
scan_install_dirs();
|
|
||||||
|
|
||||||
if !is_set_up {
|
|
||||||
return AppState {
|
|
||||||
status: AppStatus::NotConfigured,
|
|
||||||
user: None,
|
|
||||||
download_manager,
|
|
||||||
process_manager,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("database is set up");
|
|
||||||
|
|
||||||
// TODO: Account for possible failure
|
|
||||||
let (app_status, user) = auth::setup().await;
|
|
||||||
|
|
||||||
let db_handle = borrow_db_checked();
|
|
||||||
let mut missing_games = Vec::new();
|
|
||||||
let statuses = db_handle.applications.game_statuses.clone();
|
|
||||||
drop(db_handle);
|
|
||||||
|
|
||||||
for (game_id, status) in statuses {
|
|
||||||
match status {
|
|
||||||
GameDownloadStatus::Remote {} => {}
|
|
||||||
GameDownloadStatus::PartiallyInstalled { .. } => {}
|
|
||||||
GameDownloadStatus::SetupRequired {
|
|
||||||
version_name: _,
|
|
||||||
install_dir,
|
|
||||||
} => {
|
|
||||||
let install_dir_path = Path::new(&install_dir);
|
|
||||||
if !install_dir_path.exists() {
|
|
||||||
missing_games.push(game_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GameDownloadStatus::Installed {
|
|
||||||
version_name: _,
|
|
||||||
install_dir,
|
|
||||||
} => {
|
|
||||||
let install_dir_path = Path::new(&install_dir);
|
|
||||||
if !install_dir_path.exists() {
|
|
||||||
missing_games.push(game_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("detected games missing: {missing_games:?}");
|
|
||||||
|
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
|
||||||
for game_id in missing_games {
|
|
||||||
db_handle
|
|
||||||
.applications
|
|
||||||
.game_statuses
|
|
||||||
.entry(game_id)
|
|
||||||
.and_modify(|v| *v = GameDownloadStatus::Remote {});
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(db_handle);
|
|
||||||
|
|
||||||
debug!("finished setup!");
|
|
||||||
|
|
||||||
// Sync autostart state
|
|
||||||
if let Err(e) = sync_autostart_on_startup(&handle) {
|
|
||||||
warn!("failed to sync autostart state: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
AppState {
|
|
||||||
status: app_status,
|
|
||||||
user,
|
|
||||||
download_manager,
|
|
||||||
process_manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
src-tauri/src/utils/mod.rs
Normal file
1
src-tauri/src/utils/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod state_lock;
|
||||||
6
src-tauri/src/utils/state_lock.rs
Normal file
6
src-tauri/src/utils/state_lock.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! state_lock {
|
||||||
|
($state:expr) => {
|
||||||
|
$state.lock().expect("Failed to lock onto state")
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user