mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-12 15:52:43 +10:00
Compare commits
29 Commits
cloud-save
...
v0.3.0-rc-
| Author | SHA1 | Date | |
|---|---|---|---|
| eba224f998 | |||
| d045385a5d | |||
| d878806ade | |||
| b71081006e | |||
| c9e1ed78eb | |||
| 446aa70b0b | |||
| 1d0b81078a | |||
| 5251a56c3c | |||
| eeca8a7a98 | |||
| 365cdaf311 | |||
| 2957773179 | |||
| 15e5fe4dc0 | |||
| 2dc0a78354 | |||
| 51c480f245 | |||
| 95d223e2b2 | |||
| 790e8c2afe | |||
| 02edb2cbc1 | |||
| 4b4c0734ec | |||
| e75e0044fb | |||
| 65561abdab | |||
| fed3e08dce | |||
| b0b1e397b1 | |||
| 7b443818d1 | |||
| fa4a881cc0 | |||
| 4f16a6e6b2 | |||
| 47d9e9949b | |||
| a643d6102b | |||
| a71ff160c2 | |||
| a53a566792 |
70
.github/workflows/release.yml
vendored
Normal file
70
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
name: 'publish'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
# This can be used to automatically publish nightlies at UTC nighttime
|
||||||
|
# schedule:
|
||||||
|
# - cron: "0 2 * * *" # run at 2 AM UTC
|
||||||
|
|
||||||
|
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-tauri:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: 'macos-latest' # for Arm based macs (M1 and above).
|
||||||
|
args: '--target aarch64-apple-darwin'
|
||||||
|
- platform: 'macos-latest' # for Intel based macs.
|
||||||
|
args: '--target x86_64-apple-darwin'
|
||||||
|
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||||
|
args: ''
|
||||||
|
- platform: 'ubuntu-24.04-arm'
|
||||||
|
args: '--target aarch64-unknown-linux-gnu'
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: install Rust nightly
|
||||||
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with:
|
||||||
|
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||||
|
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||||
|
|
||||||
|
- name: install frontend dependencies
|
||||||
|
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||||
|
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||||
|
releaseName: 'Auto-release v__VERSION__'
|
||||||
|
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
args: ${{ matrix.args }}
|
||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,3 +1,9 @@
|
|||||||
[submodule "drop-base"]
|
[submodule "drop-base"]
|
||||||
path = drop-base
|
path = drop-base
|
||||||
url = https://github.com/drop-oss/drop-base
|
url = https://github.com/drop-oss/drop-base
|
||||||
|
[submodule "src-tauri/tailscale/libtailscale"]
|
||||||
|
path = src-tauri/tailscale/libtailscale
|
||||||
|
url = https://github.com/tailscale/libtailscale.git
|
||||||
|
[submodule "src-tauri/umu/umu-launcher"]
|
||||||
|
path = src-tauri/umu/umu-launcher
|
||||||
|
url = https://github.com/Open-Wine-Components/umu-launcher.git
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WindowControl class="h-16 w-16 p-4" />
|
<WindowControl />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="transition h-10 w-10 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-2">
|
<button class="transition h-full aspect-square text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100 p-[1.1rem]">
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@ -87,7 +87,7 @@ router.afterEach(() => {
|
|||||||
|
|
||||||
const state = useAppState();
|
const state = useAppState();
|
||||||
const profilePictureUrl: string = await useObject(
|
const profilePictureUrl: string = await useObject(
|
||||||
state.value.user?.profilePicture ?? ""
|
state.value.user?.profilePictureObjectId ?? ""
|
||||||
);
|
);
|
||||||
const adminUrl: string = await invoke("gen_drop_url", {
|
const adminUrl: string = await invoke("gen_drop_url", {
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
@ -99,11 +99,6 @@ function navigate(close: () => any, to: NavigationItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const navigation: NavigationItem[] = [
|
const navigation: NavigationItem[] = [
|
||||||
{
|
|
||||||
label: "Account settings",
|
|
||||||
route: "/account",
|
|
||||||
prefix: "",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "App settings",
|
label: "App settings",
|
||||||
route: "/settings",
|
route: "/settings",
|
||||||
|
|||||||
@ -136,15 +136,16 @@ async function auth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function authWrapper_wrapper() {
|
function authWrapper_wrapper() {
|
||||||
|
error.value = undefined;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
auth().catch((e) => {
|
auth().catch((e) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
error.value = e;
|
error.value = e;
|
||||||
if(offerManualTimeout) clearTimeout(offerManualTimeout);
|
if (offerManualTimeout) clearTimeout(offerManualTimeout);
|
||||||
});
|
});
|
||||||
offerManualTimeout = setTimeout(() => {
|
offerManualTimeout = setTimeout(() => {
|
||||||
offerManual.value = true;
|
offerManual.value = true;
|
||||||
}, 10000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function continueManual() {
|
async function continueManual() {
|
||||||
|
|||||||
@ -107,7 +107,7 @@ async function calculateGames() {
|
|||||||
}
|
}
|
||||||
for (const game of rawGames.value) {
|
for (const game of rawGames.value) {
|
||||||
if (icons[game.id]) continue;
|
if (icons[game.id]) continue;
|
||||||
icons[game.id] = await useObject(game.mIconId);
|
icons[game.id] = await useObject(game.mIconObjectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<HeaderButton v-if="showMinimise" @click="() => minimise()">
|
||||||
|
<MinusIcon />
|
||||||
|
</HeaderButton>
|
||||||
<HeaderButton @click="() => close()">
|
<HeaderButton @click="() => close()">
|
||||||
<XMarkIcon />
|
<XMarkIcon />
|
||||||
</HeaderButton>
|
</HeaderButton>
|
||||||
@ -8,11 +11,14 @@
|
|||||||
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
|
import { MinusIcon, XMarkIcon } from "@heroicons/vue/16/solid";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
|
||||||
async function close(){
|
const window = getCurrentWindow();
|
||||||
console.log(window);
|
const showMinimise = await window.isMinimizable();
|
||||||
const result = await window.close();
|
|
||||||
console.log(`closed window: ${result}`);
|
async function close() {
|
||||||
|
await window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const window = getCurrentWindow();
|
async function minimise() {
|
||||||
|
await window.minimize();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "drop-app",
|
"name": "drop-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0-beta",
|
"version": "0.3.0-rc-2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@ -37,7 +37,9 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"sass-embedded": "^1.79.4",
|
"sass-embedded": "^1.79.4",
|
||||||
"tailwindcss": "^3.4.13"
|
"tailwindcss": "^3.4.13",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mx-auto max-w-7xl px-8">
|
|
||||||
<div class="border-b border-zinc-700 py-5">
|
|
||||||
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
|
||||||
Account
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5">
|
|
||||||
<div class="divide-y divide-zinc-700">
|
|
||||||
<div class="py-6">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
|
|
||||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
|
||||||
Sign out of your Drop account on this device
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
@click="signOut"
|
|
||||||
type="button"
|
|
||||||
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<h3 class="text-sm font-medium text-red-600">
|
|
||||||
{{ error }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { listen } from '@tauri-apps/api/event'
|
|
||||||
import { useRouter } from '#imports'
|
|
||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
|
|
||||||
// Listen for auth events
|
|
||||||
onMounted(async () => {
|
|
||||||
await listen('auth/signedout', () => {
|
|
||||||
router.push('/auth/signedout')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function signOut() {
|
|
||||||
try {
|
|
||||||
error.value = null
|
|
||||||
await invoke('sign_out')
|
|
||||||
} catch (e) {
|
|
||||||
error.value = `Failed to sign out: ${e}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -156,9 +156,9 @@
|
|||||||
<template #default>
|
<template #default>
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
<div class="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<DialogTitle as="h3" class="text-base font-semibold text-zinc-100"
|
<h3 class="text-base font-semibold text-zinc-100"
|
||||||
>Install {{ game.mName }}?
|
>Install {{ game.mName }}?
|
||||||
</DialogTitle>
|
</h3>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p class="text-sm text-zinc-400">
|
<p class="text-sm text-zinc-400">
|
||||||
Drop will add {{ game.mName }} to the queue to be downloaded.
|
Drop will add {{ game.mName }} to the queue to be downloaded.
|
||||||
@ -350,7 +350,7 @@
|
|||||||
<LoadingButton
|
<LoadingButton
|
||||||
@click="() => install()"
|
@click="() => install()"
|
||||||
:disabled="
|
:disabled="
|
||||||
!(versionOptions && versionOptions.length > 0 && !installDir)
|
!(versionOptions && versionOptions.length > 0)
|
||||||
"
|
"
|
||||||
:loading="installLoading"
|
:loading="installLoading"
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -477,11 +477,11 @@ const remoteUrl: string = await invoke("gen_drop_url", {
|
|||||||
path: `/store/${game.value.id}`,
|
path: `/store/${game.value.id}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bannerUrl = await useObject(game.value.mBannerId);
|
const bannerUrl = await useObject(game.value.mBannerObjectId);
|
||||||
|
|
||||||
// Get all available images
|
// Get all available images
|
||||||
const mediaUrls = await Promise.all(
|
const mediaUrls = await Promise.all(
|
||||||
game.value.mImageCarousel.map((id) => useObject(id))
|
game.value.mImageCarouselObjectIds.map((id) => useObject(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
const htmlDescription = micromark(game.value.mDescription);
|
const htmlDescription = micromark(game.value.mDescription);
|
||||||
|
|||||||
@ -158,7 +158,7 @@ function loadGamesForQueue(v: typeof queue.value) {
|
|||||||
if (games.value[id]) return;
|
if (games.value[id]) return;
|
||||||
(async () => {
|
(async () => {
|
||||||
const gameData = await useGame(id);
|
const gameData = await useGame(id);
|
||||||
const cover = await useObject(gameData.game.mCoverId);
|
const cover = await useObject(gameData.game.mCoverObjectId);
|
||||||
games.value[id] = { ...gameData, cover };
|
games.value[id] = { ...gameData, cover };
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import type { Component } from "vue";
|
|||||||
import type { NavigationItem } from "~/types";
|
import type { NavigationItem } from "~/types";
|
||||||
import { platform } from '@tauri-apps/plugin-os';
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { UserIcon } from "@heroicons/vue/20/solid";
|
||||||
|
|
||||||
const systemData = await invoke<{
|
const systemData = await invoke<{
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@ -101,6 +102,12 @@ const navigation = computed(() => [
|
|||||||
prefix: "/settings/downloads",
|
prefix: "/settings/downloads",
|
||||||
icon: ArrowDownTrayIcon,
|
icon: ArrowDownTrayIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Account",
|
||||||
|
route: "/settings/account",
|
||||||
|
prefix: "/settings/account",
|
||||||
|
icon: UserIcon
|
||||||
|
},
|
||||||
...(isDebugMode.value ? [{
|
...(isDebugMode.value ? [{
|
||||||
label: "Debug Info",
|
label: "Debug Info",
|
||||||
route: "/settings/debug",
|
route: "/settings/debug",
|
||||||
|
|||||||
64
pages/settings/account.vue
Normal file
64
pages/settings/account.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="border-b border-zinc-700 py-5">
|
||||||
|
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
||||||
|
General
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 flex flex-col gap-4">
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium leading-6 text-zinc-100">Sign out</h3>
|
||||||
|
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||||
|
Sign out of your Drop account on this device
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="signOut"
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="rounded-md bg-red-600/10 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-600">
|
||||||
|
{{ error }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { useRouter } from "#imports";
|
||||||
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Listen for auth events
|
||||||
|
onMounted(async () => {
|
||||||
|
await listen("auth/signedout", () => {
|
||||||
|
router.push("/auth/signedout");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function signOut() {
|
||||||
|
try {
|
||||||
|
error.value = null;
|
||||||
|
await invoke("sign_out");
|
||||||
|
} catch (e) {
|
||||||
|
error.value = `Failed to sign out: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,8 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="border-b border-zinc-700 py-5">
|
||||||
<div class="border-b border-zinc-600 py-2 px-1">
|
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
||||||
|
Downloads
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="border-b border-zinc-600">
|
||||||
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
|
<div class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap">
|
||||||
<div class="ml-4 mt-2">
|
<div class="ml-4 mt-2 pb-4">
|
||||||
<h3 class="text-base font-display font-semibold text-zinc-100">
|
<h3 class="text-base font-display font-semibold text-zinc-100">
|
||||||
Install directories
|
Install directories
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="divide-y divide-zinc-700">
|
<div class="border-b border-zinc-700 py-5">
|
||||||
<div class="py-6">
|
<h3 class="text-base font-semibold font-display leading-6 text-zinc-100">
|
||||||
<h2 class="text-base font-semibold font-display leading-7 text-zinc-100">General</h2>
|
General
|
||||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
</h3>
|
||||||
Configure basic application settings
|
</div>
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mt-10 space-y-8">
|
<div class="mt-5 space-y-8">
|
||||||
<div class="flex flex-row items-center justify-between">
|
<div class="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium leading-6 text-zinc-100">Start with system</h3>
|
<h3 class="text-sm font-medium leading-6 text-zinc-100">
|
||||||
|
Start with system
|
||||||
|
</h3>
|
||||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||||
Drop will automatically start when you log into your computer
|
Drop will automatically start when you log into your computer
|
||||||
</p>
|
</p>
|
||||||
@ -18,43 +19,41 @@
|
|||||||
v-model="autostartEnabled"
|
v-model="autostartEnabled"
|
||||||
:class="[
|
:class="[
|
||||||
autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700',
|
autostartEnabled ? 'bg-blue-600' : 'bg-zinc-700',
|
||||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
autostartEnabled ? 'translate-x-5' : 'translate-x-0',
|
autostartEnabled ? 'translate-x-5' : 'translate-x-0',
|
||||||
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
|
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Switch } from '@headlessui/vue'
|
import { Switch } from "@headlessui/vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
defineProps<{}>()
|
defineProps<{}>();
|
||||||
|
|
||||||
const autostartEnabled = ref<boolean>(false)
|
const autostartEnabled = ref<boolean>(false);
|
||||||
|
|
||||||
// Load initial state
|
// Load initial state
|
||||||
invoke('get_autostart_enabled').then((enabled) => {
|
invoke("get_autostart_enabled").then((enabled) => {
|
||||||
autostartEnabled.value = enabled as boolean
|
autostartEnabled.value = enabled as boolean;
|
||||||
})
|
});
|
||||||
|
|
||||||
// Watch for changes and update autostart
|
// Watch for changes and update autostart
|
||||||
watch(autostartEnabled, async (newValue: boolean) => {
|
watch(autostartEnabled, async (newValue: boolean) => {
|
||||||
try {
|
try {
|
||||||
await invoke('toggle_autostart', { enabled: newValue })
|
await invoke("toggle_autostart", { enabled: newValue });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle autostart:', error)
|
console.error("Failed to toggle autostart:", error);
|
||||||
// Revert the toggle if it failed
|
// Revert the toggle if it failed
|
||||||
autostartEnabled.value = !newValue
|
autostartEnabled.value = !newValue;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
2547
src-tauri/Cargo.lock
generated
2547
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "drop-app"
|
name = "drop-app"
|
||||||
version = "0.2.0-beta-prerelease-1"
|
version = "0.3.0-rc-2"
|
||||||
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
description = "The client application for the open-source, self-hosted game distribution platform Drop"
|
||||||
authors = ["Drop OSS"]
|
authors = ["Drop OSS"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -16,8 +16,6 @@ tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
|||||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
name = "drop_app_lib"
|
name = "drop_app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build]
|
|
||||||
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
rustflags = ["-C", "target-feature=+aes,+sse2"]
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +27,7 @@ tauri-plugin-shell = "2.2.1"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde-binary = "0.5.0"
|
serde-binary = "0.5.0"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
|
directories = "5.0.1"
|
||||||
webbrowser = "1.0.2"
|
webbrowser = "1.0.2"
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
@ -50,25 +49,14 @@ throttle_my_fn = "0.2.6"
|
|||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
atomic-instant-full = "0.1.0"
|
atomic-instant-full = "0.1.0"
|
||||||
cacache = "13.1.0"
|
cacache = "13.1.0"
|
||||||
bincode = "1.3.3"
|
|
||||||
http-serde = "2.1.1"
|
http-serde = "2.1.1"
|
||||||
reqwest-middleware = "0.4.0"
|
reqwest-middleware = "0.4.0"
|
||||||
reqwest-middleware-cache = "0.1.1"
|
reqwest-middleware-cache = "0.1.1"
|
||||||
deranged = "=0.4.0"
|
deranged = "=0.4.0"
|
||||||
droplet-rs = "0.7.3"
|
droplet-rs = "0.7.3"
|
||||||
gethostname = "1.0.1"
|
gethostname = "1.0.1"
|
||||||
zstd = "0.13.3"
|
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
|
||||||
tar = "0.4.44"
|
# tailscale = { path = "./tailscale" }
|
||||||
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"
|
|
||||||
|
|
||||||
[dependencies.dynfmt]
|
[dependencies.dynfmt]
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -92,15 +80,20 @@ features = ["fs"]
|
|||||||
|
|
||||||
[dependencies.uuid]
|
[dependencies.uuid]
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
features = ["v4", "fast-rng", "macro-diagnostics"]
|
features = [
|
||||||
|
"v4", # Lets you generate random UUIDs
|
||||||
|
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||||
|
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies.rustbreak]
|
[dependencies.rustbreak]
|
||||||
version = "2"
|
version = "2"
|
||||||
features = [] # You can also use "yaml_enc" or "bin_enc"
|
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
version = "0.12"
|
version = "0.12"
|
||||||
features = ["json", "blocking"]
|
default-features = false
|
||||||
|
features = ["json", "http2", "blocking", "rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|||||||
3
src-tauri/src/client/mod.rs
Normal file
3
src-tauri/src/client/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod autostart;
|
||||||
|
pub mod cleanup;
|
||||||
|
pub mod commands;
|
||||||
@ -1,102 +0,0 @@
|
|||||||
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
|
|
||||||
|
|
||||||
use super::path::CommonPath;
|
|
||||||
|
|
||||||
pub struct BackupManager<'a> {
|
|
||||||
pub current_platform: Platform,
|
|
||||||
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackupManager<'_> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
BackupManager {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
current_platform: Platform::Windows,
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
current_platform: Platform::MacOs,
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
current_platform: Platform::Linux,
|
|
||||||
|
|
||||||
sources: HashMap::from([
|
|
||||||
// Current platform to target platform
|
|
||||||
(
|
|
||||||
(Platform::Windows, Platform::Windows),
|
|
||||||
&WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(Platform::Linux, Platform::Linux),
|
|
||||||
&LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
(Platform::MacOs, Platform::MacOs),
|
|
||||||
&MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
|
|
||||||
),
|
|
||||||
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait BackupHandler: Send + Sync {
|
|
||||||
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.lock().unwrap().join("games")) }
|
|
||||||
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&game.game_id).unwrap()) }
|
|
||||||
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) }
|
|
||||||
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }
|
|
||||||
fn store_user_id_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError) }
|
|
||||||
fn os_user_name_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&whoami::username()).unwrap()) }
|
|
||||||
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winAppData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppDataLow>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDocuments>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winPublic>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winProgramData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDir>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgData>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgConfig>"); Err(BackupError::InvalidSystem) }
|
|
||||||
fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::new()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LinuxBackupManager {}
|
|
||||||
impl BackupHandler for LinuxBackupManager {
|
|
||||||
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Data.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct WindowsBackupManager {}
|
|
||||||
impl BackupHandler for WindowsBackupManager {
|
|
||||||
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::DataLocal.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::DataLocalLow.get().ok_or(BackupError::NotFound)?)
|
|
||||||
}
|
|
||||||
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(PathBuf::from_str("C:/Windows").unwrap())
|
|
||||||
}
|
|
||||||
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Document.get().ok_or(BackupError::NotFound)?)
|
|
||||||
|
|
||||||
}
|
|
||||||
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(PathBuf::from_str("C:/ProgramData").unwrap())
|
|
||||||
}
|
|
||||||
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
|
|
||||||
Ok(CommonPath::Public.get().ok_or(BackupError::NotFound)?)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct MacBackupManager {}
|
|
||||||
impl BackupHandler for MacBackupManager {}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
use crate::process::process_manager::Platform;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Condition {
|
|
||||||
Os(Platform)
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
use crate::database::db::GameVersion;
|
|
||||||
|
|
||||||
use super::conditions::{Condition};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct CloudSaveMetadata {
|
|
||||||
pub files: Vec<GameFile>,
|
|
||||||
pub game_version: GameVersion,
|
|
||||||
pub save_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct GameFile {
|
|
||||||
pub path: String,
|
|
||||||
pub id: Option<String>,
|
|
||||||
pub data_type: DataType,
|
|
||||||
pub tags: Vec<Tag>,
|
|
||||||
pub conditions: Vec<Condition>
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum DataType {
|
|
||||||
Registry,
|
|
||||||
File,
|
|
||||||
Other
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum Tag {
|
|
||||||
Config,
|
|
||||||
Save,
|
|
||||||
#[default]
|
|
||||||
#[serde(other)]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
pub mod conditions;
|
|
||||||
pub mod metadata;
|
|
||||||
pub mod resolver;
|
|
||||||
pub mod placeholder;
|
|
||||||
pub mod normalise;
|
|
||||||
pub mod path;
|
|
||||||
pub mod backup_manager;
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
use crate::process::process_manager::Platform;
|
|
||||||
|
|
||||||
use super::placeholder::*;
|
|
||||||
|
|
||||||
|
|
||||||
pub fn normalize(path: &str, os: Platform) -> String {
|
|
||||||
let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/");
|
|
||||||
|
|
||||||
if path == "~" || path.starts_with("~/") {
|
|
||||||
path = path.replacen('~', HOME, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static CONSECUTIVE_SLASHES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/{2,}").unwrap());
|
|
||||||
static UNNECESSARY_DOUBLE_STAR_1: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap());
|
|
||||||
static UNNECESSARY_DOUBLE_STAR_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap());
|
|
||||||
static ENDING_WILDCARD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap());
|
|
||||||
static ENDING_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap());
|
|
||||||
static INTERMEDIATE_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\./)").unwrap());
|
|
||||||
static BLANK_SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap());
|
|
||||||
static APP_DATA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap());
|
|
||||||
static APP_DATA_ROAMING: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap());
|
|
||||||
static APP_DATA_LOCAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap());
|
|
||||||
static APP_DATA_LOCAL_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap());
|
|
||||||
static USER_PROFILE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap());
|
|
||||||
static DOCUMENTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap());
|
|
||||||
|
|
||||||
for (pattern, replacement) in [
|
|
||||||
(&CONSECUTIVE_SLASHES, "/"),
|
|
||||||
(&UNNECESSARY_DOUBLE_STAR_1, "${1}*"),
|
|
||||||
(&UNNECESSARY_DOUBLE_STAR_2, "*${1}"),
|
|
||||||
(&ENDING_WILDCARD, ""),
|
|
||||||
(&ENDING_DOT, ""),
|
|
||||||
(&INTERMEDIATE_DOT, "/"),
|
|
||||||
(&BLANK_SEGMENT, "/"),
|
|
||||||
(&APP_DATA, WIN_APP_DATA),
|
|
||||||
(&APP_DATA_ROAMING, WIN_APP_DATA),
|
|
||||||
(&APP_DATA_LOCAL, WIN_LOCAL_APP_DATA),
|
|
||||||
(&APP_DATA_LOCAL_2, &format!("{}/", WIN_LOCAL_APP_DATA)),
|
|
||||||
(&USER_PROFILE, HOME),
|
|
||||||
(&DOCUMENTS, WIN_DOCUMENTS),
|
|
||||||
] {
|
|
||||||
path = pattern.replace_all(&path, replacement).to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if os == Platform::Windows {
|
|
||||||
let documents_2: Regex = Regex::new(r"(?i)<home>/Documents").unwrap();
|
|
||||||
|
|
||||||
#[allow(clippy::single_element_loop)]
|
|
||||||
for (pattern, replacement) in [(&documents_2, WIN_DOCUMENTS)] {
|
|
||||||
path = pattern.replace_all(&path, replacement).to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (pattern, replacement) in [
|
|
||||||
("{64BitSteamID}", STORE_USER_ID),
|
|
||||||
("{Steam3AccountID}", STORE_USER_ID),
|
|
||||||
] {
|
|
||||||
path = path.replace(pattern, replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn too_broad(path: &str) -> bool {
|
|
||||||
println!("Path: {}", path);
|
|
||||||
use {BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA};
|
|
||||||
|
|
||||||
let path_lower = path.to_lowercase();
|
|
||||||
|
|
||||||
for item in ALL {
|
|
||||||
if path == *item {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for item in AVOID_WILDCARDS {
|
|
||||||
if path.starts_with(&format!("{}/*", item)) || path.starts_with(&format!("{}/{}", item, STORE_USER_ID)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// These paths are present whether or not the game is installed.
|
|
||||||
// If possible, they should be narrowed down on the wiki.
|
|
||||||
for item in [
|
|
||||||
format!("{}/{}", BASE, STORE_USER_ID), // because `<storeUserId>` is handled as `*`
|
|
||||||
format!("{}/Documents", HOME),
|
|
||||||
format!("{}/Saved Games", HOME),
|
|
||||||
format!("{}/AppData", HOME),
|
|
||||||
format!("{}/AppData/Local", HOME),
|
|
||||||
format!("{}/AppData/Local/Packages", HOME),
|
|
||||||
format!("{}/AppData/LocalLow", HOME),
|
|
||||||
format!("{}/AppData/Roaming", HOME),
|
|
||||||
format!("{}/Documents/My Games", HOME),
|
|
||||||
format!("{}/Library/Application Support", HOME),
|
|
||||||
format!("{}/Library/Application Support/UserData", HOME),
|
|
||||||
format!("{}/Library/Preferences", HOME),
|
|
||||||
format!("{}/.renpy", HOME),
|
|
||||||
format!("{}/.renpy/persistent", HOME),
|
|
||||||
format!("{}/Library", HOME),
|
|
||||||
format!("{}/Library/RenPy", HOME),
|
|
||||||
format!("{}/Telltale Games", HOME),
|
|
||||||
format!("{}/config", ROOT),
|
|
||||||
format!("{}/MMFApplications", WIN_APP_DATA),
|
|
||||||
format!("{}/RenPy", WIN_APP_DATA),
|
|
||||||
format!("{}/RenPy/persistent", WIN_APP_DATA),
|
|
||||||
format!("{}/win.ini", WIN_DIR),
|
|
||||||
format!("{}/SysWOW64", WIN_DIR),
|
|
||||||
format!("{}/My Games", WIN_DOCUMENTS),
|
|
||||||
format!("{}/Telltale Games", WIN_DOCUMENTS),
|
|
||||||
format!("{}/unity3d", XDG_CONFIG),
|
|
||||||
format!("{}/unity3d", XDG_DATA),
|
|
||||||
"C:/Program Files".to_string(),
|
|
||||||
"C:/Program Files (x86)".to_string(),
|
|
||||||
] {
|
|
||||||
let item = item.to_lowercase();
|
|
||||||
if path_lower == item
|
|
||||||
|| path_lower.starts_with(&format!("{}/*", item))
|
|
||||||
|| path_lower.starts_with(&format!("{}/{}", item, STORE_USER_ID.to_lowercase()))
|
|
||||||
|| path_lower.starts_with(&format!("{}/savesdir", item))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Drive letters:
|
|
||||||
let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap();
|
|
||||||
if drives.is_match(path) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Colon not for a drive letter
|
|
||||||
if path.get(2..).is_some_and(|path| path.contains(':')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root:
|
|
||||||
if path == "/" {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relative path wildcard:
|
|
||||||
if path.starts_with('*') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn usable(path: &str) -> bool {
|
|
||||||
let unprintable: Regex = Regex::new(r"(\p{Cc}|\p{Cf})").unwrap();
|
|
||||||
|
|
||||||
!path.is_empty()
|
|
||||||
&& !path.contains("{{")
|
|
||||||
&& !path.starts_with("./")
|
|
||||||
&& !path.starts_with("../")
|
|
||||||
&& !too_broad(path)
|
|
||||||
&& !unprintable.is_match(path)
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
use std::{path::PathBuf, sync::LazyLock};
|
|
||||||
|
|
||||||
pub enum CommonPath {
|
|
||||||
Config,
|
|
||||||
Data,
|
|
||||||
DataLocal,
|
|
||||||
DataLocalLow,
|
|
||||||
Document,
|
|
||||||
Home,
|
|
||||||
Public,
|
|
||||||
SavedGames,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommonPath {
|
|
||||||
pub fn get(&self) -> Option<PathBuf> {
|
|
||||||
static CONFIG: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::config_dir());
|
|
||||||
static DATA: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_dir());
|
|
||||||
static DATA_LOCAL: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_local_dir());
|
|
||||||
static DOCUMENT: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::document_dir());
|
|
||||||
static HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::home_dir());
|
|
||||||
static PUBLIC: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::public_dir());
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
static DATA_LOCAL_LOW: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
|
|
||||||
known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppDataLow)
|
|
||||||
});
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
static DATA_LOCAL_LOW: Option<PathBuf> = None;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
static SAVED_GAMES: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
|
|
||||||
known_folders::get_known_folder_path(known_folders::KnownFolder::SavedGames)
|
|
||||||
});
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
static SAVED_GAMES: Option<PathBuf> = None;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::Config => CONFIG.clone(),
|
|
||||||
Self::Data => DATA.clone(),
|
|
||||||
Self::DataLocal => DATA_LOCAL.clone(),
|
|
||||||
Self::DataLocalLow => DATA_LOCAL_LOW.clone(),
|
|
||||||
Self::Document => DOCUMENT.clone(),
|
|
||||||
Self::Home => HOME.clone(),
|
|
||||||
Self::Public => PUBLIC.clone(),
|
|
||||||
Self::SavedGames => SAVED_GAMES.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
pub const ALL: &[&str] = &[
|
|
||||||
ROOT,
|
|
||||||
GAME,
|
|
||||||
BASE,
|
|
||||||
HOME,
|
|
||||||
STORE_USER_ID,
|
|
||||||
OS_USER_NAME,
|
|
||||||
WIN_APP_DATA,
|
|
||||||
WIN_LOCAL_APP_DATA,
|
|
||||||
WIN_DOCUMENTS,
|
|
||||||
WIN_PUBLIC,
|
|
||||||
WIN_PROGRAM_DATA,
|
|
||||||
WIN_DIR,
|
|
||||||
XDG_DATA,
|
|
||||||
XDG_CONFIG,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// These are paths where `<placeholder>/*/` is suspicious.
|
|
||||||
pub const AVOID_WILDCARDS: &[&str] = &[
|
|
||||||
ROOT,
|
|
||||||
HOME,
|
|
||||||
WIN_APP_DATA,
|
|
||||||
WIN_LOCAL_APP_DATA,
|
|
||||||
WIN_DOCUMENTS,
|
|
||||||
WIN_PUBLIC,
|
|
||||||
WIN_PROGRAM_DATA,
|
|
||||||
WIN_DIR,
|
|
||||||
XDG_DATA,
|
|
||||||
XDG_CONFIG,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub const ROOT: &str = "<root>"; // a directory where games are installed (configured in backup tool)
|
|
||||||
pub const GAME: &str = "<game>"; // an installDir (if defined) or the game's canonical name in the manifest
|
|
||||||
pub const BASE: &str = "<base>"; // shorthand for <root>/<game> (unless overridden by store-specific rules)
|
|
||||||
pub const HOME: &str = "<home>"; // current user's home directory in the OS (~)
|
|
||||||
pub const STORE_USER_ID: &str = "<storeUserId>"; // a store-specific id from the manifest, corresponding to the root's store type
|
|
||||||
pub const OS_USER_NAME: &str = "<osUserName>"; // current user's ID in the game store
|
|
||||||
pub const WIN_APP_DATA: &str = "<winAppData>"; // current user's name in the OS
|
|
||||||
pub const WIN_LOCAL_APP_DATA: &str = "<winLocalAppData>"; // %APPDATA% on Windows
|
|
||||||
pub const WIN_LOCAL_APP_DATA_LOW: &str = "<winLocalAppDataLow>"; // %LOCALAPPDATA% on Windows
|
|
||||||
pub const WIN_DOCUMENTS: &str = "<winDocuments>"; // <home>/AppData/LocalLow on Windows
|
|
||||||
pub const WIN_PUBLIC: &str = "<winPublic>"; // <home>/Documents (f.k.a. <home>/My Documents) or a localized equivalent on Windows
|
|
||||||
pub const WIN_PROGRAM_DATA: &str = "<winProgramData>"; // %PUBLIC% on Windows
|
|
||||||
pub const WIN_DIR: &str = "<winDir>"; // %PROGRAMDATA% on Windows
|
|
||||||
pub const XDG_DATA: &str = "<xdgData>"; // %WINDIR% on Windows
|
|
||||||
pub const XDG_CONFIG: &str = "<xdgConfig>"; // $XDG_DATA_HOME on Linux
|
|
||||||
pub const SKIP: &str = "<skip>"; // $XDG_CONFIG_HOME on Linux
|
|
||||||
|
|
||||||
pub static OS_USERNAME: LazyLock<String> = LazyLock::new(|| whoami::username());
|
|
||||||
@ -1,255 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs::{self, create_dir_all, File},
|
|
||||||
io::{self, ErrorKind, Read, Write},
|
|
||||||
path::{Path, PathBuf}, thread::sleep, time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*,
|
|
||||||
};
|
|
||||||
use log::{debug, warn};
|
|
||||||
use rustix::path::Arg;
|
|
||||||
use tempfile::tempfile;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::db::GameVersion, error::backup_error::BackupError, process::process_manager::Platform,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize};
|
|
||||||
|
|
||||||
pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
|
|
||||||
let f = File::create_new("save").unwrap();
|
|
||||||
let compressor = zstd::Encoder::new(f, 22).unwrap();
|
|
||||||
let mut tarball = tar::Builder::new(compressor);
|
|
||||||
let manager = BackupManager::new();
|
|
||||||
for file in meta.files.iter_mut() {
|
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
|
||||||
let os = match file
|
|
||||||
.conditions
|
|
||||||
.iter()
|
|
||||||
.find_map(|p| match p {
|
|
||||||
super::conditions::Condition::Os(os) => Some(os),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
Some(os) => os,
|
|
||||||
None => {
|
|
||||||
warn!(
|
|
||||||
"File {:?} could not be backed up because it did not provide an OS",
|
|
||||||
&file
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let handler = match manager.sources.get(&(manager.current_platform, os)) {
|
|
||||||
Some(h) => *h,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
let t_path = PathBuf::from(normalize(&file.path, os));
|
|
||||||
println!("{:?}", &t_path);
|
|
||||||
let path = parse_path(t_path, handler, &meta.game_version).unwrap();
|
|
||||||
let f = std::fs::metadata(&path).unwrap(); // TODO: Fix unwrap here
|
|
||||||
if f.is_dir() {
|
|
||||||
tarball.append_dir_all(&id, path).unwrap();
|
|
||||||
} else if f.is_file() {
|
|
||||||
tarball
|
|
||||||
.append_file(&id, &mut File::open(path).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
file.id = Some(id);
|
|
||||||
}
|
|
||||||
let binding = serde_json::to_string(meta).unwrap();
|
|
||||||
println!("Binding: {}", &binding);
|
|
||||||
let serialized = binding.as_bytes();
|
|
||||||
let mut file = tempfile().unwrap();
|
|
||||||
file.write(serialized).unwrap();
|
|
||||||
tarball.append_file("metadata", &mut file).unwrap();
|
|
||||||
tarball.into_inner().unwrap().finish().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract(file: PathBuf) -> Result<(), BackupError> {
|
|
||||||
let tmpdir = tempfile::tempdir().unwrap();
|
|
||||||
|
|
||||||
// Reopen the file for reading
|
|
||||||
let file = File::open(file).unwrap();
|
|
||||||
|
|
||||||
let decompressor = zstd::Decoder::new(file).unwrap();
|
|
||||||
let mut f = tar::Archive::new(decompressor);
|
|
||||||
f.unpack(tmpdir.path()).unwrap();
|
|
||||||
|
|
||||||
let path = tmpdir.path();
|
|
||||||
|
|
||||||
let mut manifest = File::open(path.join("metadata")).unwrap();
|
|
||||||
|
|
||||||
let mut manifest_slice = Vec::new();
|
|
||||||
manifest.read_to_end(&mut manifest_slice).unwrap();
|
|
||||||
|
|
||||||
let manifest: CloudSaveMetadata = serde_json::from_slice(&manifest_slice).unwrap();
|
|
||||||
|
|
||||||
for file in manifest.files {
|
|
||||||
let current_path = path.join(file.id.as_ref().unwrap());
|
|
||||||
|
|
||||||
let manager = BackupManager::new();
|
|
||||||
let os = match file
|
|
||||||
.conditions
|
|
||||||
.iter()
|
|
||||||
.find_map(|p| match p {
|
|
||||||
super::conditions::Condition::Os(os) => Some(os),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
Some(os) => os,
|
|
||||||
None => {
|
|
||||||
warn!(
|
|
||||||
"File {:?} could not be replaced up because it did not provide an OS",
|
|
||||||
&file
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let handler = match manager.sources.get(&(manager.current_platform, os)) {
|
|
||||||
Some(h) => *h,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?;
|
|
||||||
create_dir_all(&new_path.parent().unwrap()).unwrap();
|
|
||||||
|
|
||||||
println!("Current path {:?} copying to {:?}", ¤t_path, &new_path);
|
|
||||||
|
|
||||||
|
|
||||||
copy_item(current_path, new_path).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copy_item<P: AsRef<Path>>(src: P, dest: P) -> io::Result<()> {
|
|
||||||
let src_path = src.as_ref();
|
|
||||||
let dest_path = dest.as_ref();
|
|
||||||
|
|
||||||
let metadata = fs::metadata(&src_path)?;
|
|
||||||
|
|
||||||
if metadata.is_file() {
|
|
||||||
// Ensure the parent directory of the destination exists for a file copy
|
|
||||||
if let Some(parent) = dest_path.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
fs::copy(&src_path, &dest_path)?;
|
|
||||||
} else if metadata.is_dir() {
|
|
||||||
// For directories, we call the recursive helper function.
|
|
||||||
// The destination for the recursive copy is the `dest_path` itself.
|
|
||||||
copy_dir_recursive(&src_path, &dest_path)?;
|
|
||||||
} else {
|
|
||||||
// Handle other file types like symlinks if necessary,
|
|
||||||
// for now, return an error or skip.
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::Other,
|
|
||||||
format!("Source {:?} is neither a file nor a directory", src_path),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
|
||||||
fs::create_dir_all(&dest)?;
|
|
||||||
|
|
||||||
for entry in fs::read_dir(src)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let entry_path = entry.path();
|
|
||||||
let entry_file_name = match entry_path.file_name() {
|
|
||||||
Some(name) => name,
|
|
||||||
None => continue, // Skip if somehow there's no file name
|
|
||||||
};
|
|
||||||
let dest_entry_path = dest.join(entry_file_name);
|
|
||||||
let metadata = entry.metadata()?;
|
|
||||||
|
|
||||||
if metadata.is_file() {
|
|
||||||
debug!("Writing file {} to {}", entry_path.display(), dest_entry_path.display());
|
|
||||||
fs::copy(&entry_path, &dest_entry_path)?;
|
|
||||||
} else if metadata.is_dir() {
|
|
||||||
copy_dir_recursive(&entry_path, &dest_entry_path)?;
|
|
||||||
}
|
|
||||||
// Ignore other types like symlinks for this basic implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_path(
|
|
||||||
path: PathBuf,
|
|
||||||
backup_handler: &dyn BackupHandler,
|
|
||||||
game: &GameVersion,
|
|
||||||
) -> Result<PathBuf, BackupError> {
|
|
||||||
println!("Parsing: {:?}", &path);
|
|
||||||
let mut s = PathBuf::new();
|
|
||||||
for component in path.components() {
|
|
||||||
match component.as_str().unwrap() {
|
|
||||||
ROOT => s.push(backup_handler.root_translate(&path, game)?),
|
|
||||||
GAME => s.push(backup_handler.game_translate(&path, game)?),
|
|
||||||
BASE => s.push(backup_handler.base_translate(&path, game)?),
|
|
||||||
HOME => s.push(backup_handler.home_translate(&path, game)?),
|
|
||||||
STORE_USER_ID => s.push(backup_handler.store_user_id_translate(&path, game)?),
|
|
||||||
OS_USER_NAME => s.push(backup_handler.os_user_name_translate(&path, game)?),
|
|
||||||
WIN_APP_DATA => s.push(backup_handler.win_app_data_translate(&path, game)?),
|
|
||||||
WIN_LOCAL_APP_DATA => s.push(backup_handler.win_local_app_data_translate(&path, game)?),
|
|
||||||
WIN_LOCAL_APP_DATA_LOW => {
|
|
||||||
s.push(backup_handler.win_local_app_data_low_translate(&path, game)?)
|
|
||||||
}
|
|
||||||
WIN_DOCUMENTS => s.push(backup_handler.win_documents_translate(&path, game)?),
|
|
||||||
WIN_PUBLIC => s.push(backup_handler.win_public_translate(&path, game)?),
|
|
||||||
WIN_PROGRAM_DATA => s.push(backup_handler.win_program_data_translate(&path, game)?),
|
|
||||||
WIN_DIR => s.push(backup_handler.win_dir_translate(&path, game)?),
|
|
||||||
XDG_DATA => s.push(backup_handler.xdg_data_translate(&path, game)?),
|
|
||||||
XDG_CONFIG => s.push(backup_handler.xdg_config_translate(&path, game)?),
|
|
||||||
SKIP => s.push(backup_handler.skip_translate(&path, game)?),
|
|
||||||
_ => s.push(PathBuf::from(component.as_os_str())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Final line: {:?}", &s);
|
|
||||||
Ok(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn test() {
|
|
||||||
let mut meta = CloudSaveMetadata {
|
|
||||||
files: vec![
|
|
||||||
GameFile {
|
|
||||||
path: String::from("<home>/favicon.png"),
|
|
||||||
id: None,
|
|
||||||
data_type: super::metadata::DataType::File,
|
|
||||||
tags: Vec::new(),
|
|
||||||
conditions: vec![Condition::Os(Platform::Linux)],
|
|
||||||
},
|
|
||||||
GameFile {
|
|
||||||
path: String::from("<home>/Documents/Pixel Art"),
|
|
||||||
id: None,
|
|
||||||
data_type: super::metadata::DataType::File,
|
|
||||||
tags: Vec::new(),
|
|
||||||
conditions: vec![Condition::Os(Platform::Linux)],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
game_version: GameVersion {
|
|
||||||
game_id: String::new(),
|
|
||||||
version_name: String::new(),
|
|
||||||
platform: Platform::Linux,
|
|
||||||
launch_command: String::new(),
|
|
||||||
launch_args: Vec::new(),
|
|
||||||
launch_command_template: String::new(),
|
|
||||||
setup_command: String::new(),
|
|
||||||
setup_args: Vec::new(),
|
|
||||||
setup_command_template: String::new(),
|
|
||||||
only_setup: true,
|
|
||||||
version_index: 0,
|
|
||||||
delta: false,
|
|
||||||
umu_id_override: None,
|
|
||||||
},
|
|
||||||
save_id: String::from("aaaaaaa"),
|
|
||||||
};
|
|
||||||
//resolve(&mut meta);
|
|
||||||
|
|
||||||
extract("save".into()).unwrap();
|
|
||||||
}
|
|
||||||
@ -6,14 +6,12 @@ use std::{
|
|||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError};
|
||||||
database::{db::borrow_db_mut_checked, settings::Settings},
|
|
||||||
download_manager::internal_error::InternalError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
|
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
|
||||||
debug::SystemData,
|
debug::SystemData,
|
||||||
|
models::data::Settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Will, in future, return disk/remaining size
|
// Will, in future, return disk/remaining size
|
||||||
@ -33,7 +31,7 @@ pub fn delete_download_dir(index: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
|
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>> {
|
||||||
// Check the new directory is all good
|
// Check the new directory is all good
|
||||||
let new_dir_path = Path::new(&new_dir);
|
let new_dir_path = Path::new(&new_dir);
|
||||||
if new_dir_path.exists() {
|
if new_dir_path.exists() {
|
||||||
|
|||||||
@ -1,150 +1,41 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
fs::{self, create_dir_all},
|
fs::{self, create_dir_all},
|
||||||
hash::Hash,
|
path::PathBuf,
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use directories::BaseDirs;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use serde_with::serde_as;
|
|
||||||
use tauri::AppHandle;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::DB;
|
||||||
database::settings::Settings,
|
|
||||||
download_manager::downloadable_metadata::DownloadableMetadata,
|
|
||||||
games::{library::push_game_update, state::GameStatusManager},
|
|
||||||
process::process_manager::Platform,
|
|
||||||
DB,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone, Deserialize)]
|
use super::models::data::Database;
|
||||||
pub struct DatabaseAuth {
|
|
||||||
pub private: String,
|
|
||||||
pub cert: String,
|
|
||||||
pub client_id: String,
|
|
||||||
pub web_token: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strings are version names for a particular game
|
|
||||||
#[derive(Serialize, Clone, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum GameDownloadStatus {
|
|
||||||
Remote {},
|
|
||||||
SetupRequired {
|
|
||||||
version_name: String,
|
|
||||||
install_dir: String,
|
|
||||||
},
|
|
||||||
Installed {
|
|
||||||
version_name: String,
|
|
||||||
install_dir: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stuff that shouldn't be synced to disk
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub enum ApplicationTransientStatus {
|
|
||||||
Downloading { version_name: String },
|
|
||||||
Uninstalling {},
|
|
||||||
Updating { version_name: String },
|
|
||||||
Running {},
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_template() -> String {
|
|
||||||
"{}".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct GameVersion {
|
|
||||||
pub game_id: String,
|
|
||||||
pub version_name: String,
|
|
||||||
|
|
||||||
pub platform: Platform,
|
|
||||||
|
|
||||||
pub launch_command: String,
|
|
||||||
pub launch_args: Vec<String>,
|
|
||||||
#[serde(default = "default_template")]
|
|
||||||
pub launch_command_template: String,
|
|
||||||
|
|
||||||
pub setup_command: String,
|
|
||||||
pub setup_args: Vec<String>,
|
|
||||||
#[serde(default = "default_template")]
|
|
||||||
pub setup_command_template: String,
|
|
||||||
|
|
||||||
pub only_setup: bool,
|
|
||||||
|
|
||||||
pub version_index: usize,
|
|
||||||
pub delta: bool,
|
|
||||||
|
|
||||||
pub umu_id_override: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Serialize, Clone, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DatabaseApplications {
|
|
||||||
pub install_dirs: Vec<PathBuf>,
|
|
||||||
// Guaranteed to exist if the game also exists in the app state map
|
|
||||||
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
|
||||||
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
|
||||||
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
|
||||||
pub struct Database {
|
|
||||||
#[serde(default)]
|
|
||||||
pub settings: Settings,
|
|
||||||
pub auth: Option<DatabaseAuth>,
|
|
||||||
pub base_url: String,
|
|
||||||
pub applications: DatabaseApplications,
|
|
||||||
pub prev_database: Option<PathBuf>,
|
|
||||||
pub cache_dir: PathBuf,
|
|
||||||
}
|
|
||||||
impl Database {
|
|
||||||
fn new<T: Into<PathBuf>>(
|
|
||||||
games_base_dir: T,
|
|
||||||
prev_database: Option<PathBuf>,
|
|
||||||
cache_dir: PathBuf,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
applications: DatabaseApplications {
|
|
||||||
install_dirs: vec![games_base_dir.into()],
|
|
||||||
game_statuses: HashMap::new(),
|
|
||||||
game_versions: HashMap::new(),
|
|
||||||
installed_game_version: HashMap::new(),
|
|
||||||
transient_statuses: HashMap::new(),
|
|
||||||
},
|
|
||||||
prev_database,
|
|
||||||
base_url: "".to_owned(),
|
|
||||||
auth: None,
|
|
||||||
settings: Settings::default(),
|
|
||||||
cache_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
|
||||||
LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
|
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
|
||||||
|
|
||||||
|
|
||||||
// Custom JSON serializer to support everything we need
|
// Custom JSON serializer to support everything we need
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct DropDatabaseSerializer;
|
pub struct DropDatabaseSerializer;
|
||||||
|
|
||||||
impl<T: Serialize + DeserializeOwned> DeSerializer<T> for DropDatabaseSerializer {
|
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
|
||||||
|
for DropDatabaseSerializer
|
||||||
|
{
|
||||||
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
|
||||||
serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize<R: std::io::Read>(&self, s: R) -> rustbreak::error::DeSerResult<T> {
|
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
|
||||||
serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string()))
|
let mut buf = Vec::new();
|
||||||
|
s.read_to_end(&mut buf)
|
||||||
|
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
|
||||||
|
let (val, _version) =
|
||||||
|
native_model::decode::<T>(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||||
|
Ok(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,12 +54,14 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
let games_base_dir = data_root_dir.join("games");
|
let games_base_dir = data_root_dir.join("games");
|
||||||
let logs_root_dir = data_root_dir.join("logs");
|
let logs_root_dir = data_root_dir.join("logs");
|
||||||
let cache_dir = data_root_dir.join("cache");
|
let cache_dir = data_root_dir.join("cache");
|
||||||
|
let pfx_dir = data_root_dir.join("pfx");
|
||||||
|
|
||||||
debug!("creating data directory at {:?}", data_root_dir);
|
debug!("creating data directory at {:?}", data_root_dir);
|
||||||
create_dir_all(data_root_dir.clone()).unwrap();
|
create_dir_all(data_root_dir.clone()).unwrap();
|
||||||
create_dir_all(&games_base_dir).unwrap();
|
create_dir_all(&games_base_dir).unwrap();
|
||||||
create_dir_all(&logs_root_dir).unwrap();
|
create_dir_all(&logs_root_dir).unwrap();
|
||||||
create_dir_all(&cache_dir).unwrap();
|
create_dir_all(&cache_dir).unwrap();
|
||||||
|
create_dir_all(&pfx_dir).unwrap();
|
||||||
|
|
||||||
let exists = fs::exists(db_path.clone()).unwrap();
|
let exists = fs::exists(db_path.clone()).unwrap();
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod settings;
|
pub mod models;
|
||||||
|
|||||||
239
src-tauri/src/database/models.rs
Normal file
239
src-tauri/src/database/models.rs
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
pub mod data {
|
||||||
|
use native_model::{native_model, Model};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub type GameVersion = v1::GameVersion;
|
||||||
|
pub type Database = v2::Database;
|
||||||
|
pub type Settings = v1::Settings;
|
||||||
|
pub type DatabaseAuth = v1::DatabaseAuth;
|
||||||
|
|
||||||
|
pub type GameDownloadStatus = v1::GameDownloadStatus;
|
||||||
|
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
|
||||||
|
pub type DownloadableMetadata = v1::DownloadableMetadata;
|
||||||
|
pub type DownloadType = v1::DownloadType;
|
||||||
|
pub type DatabaseApplications = v1::DatabaseApplications;
|
||||||
|
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
|
||||||
|
|
||||||
|
pub mod v1 {
|
||||||
|
use crate::process::process_manager::Platform;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn default_template() -> String {
|
||||||
|
"{}".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[native_model(id = 2, version = 1)]
|
||||||
|
pub struct GameVersion {
|
||||||
|
pub game_id: String,
|
||||||
|
pub version_name: String,
|
||||||
|
|
||||||
|
pub platform: Platform,
|
||||||
|
|
||||||
|
pub launch_command: String,
|
||||||
|
pub launch_args: Vec<String>,
|
||||||
|
#[serde(default = "default_template")]
|
||||||
|
pub launch_command_template: String,
|
||||||
|
|
||||||
|
pub setup_command: String,
|
||||||
|
pub setup_args: Vec<String>,
|
||||||
|
#[serde(default = "default_template")]
|
||||||
|
pub setup_command_template: String,
|
||||||
|
|
||||||
|
pub only_setup: bool,
|
||||||
|
|
||||||
|
pub version_index: usize,
|
||||||
|
pub delta: bool,
|
||||||
|
|
||||||
|
pub umu_id_override: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Serialize, Clone, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[native_model(id = 3, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
|
||||||
|
pub struct DatabaseApplications {
|
||||||
|
pub install_dirs: Vec<PathBuf>,
|
||||||
|
// Guaranteed to exist if the game also exists in the app state map
|
||||||
|
pub game_statuses: HashMap<String, GameDownloadStatus>,
|
||||||
|
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
|
||||||
|
pub installed_game_version: HashMap<String, DownloadableMetadata>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[native_model(id = 4, version = 1)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub autostart: bool,
|
||||||
|
pub max_download_threads: usize,
|
||||||
|
pub force_offline: bool, // ... other settings ...
|
||||||
|
}
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
autostart: false,
|
||||||
|
max_download_threads: 4,
|
||||||
|
force_offline: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings are version names for a particular game
|
||||||
|
#[derive(Serialize, Clone, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
#[native_model(id = 5, version = 1)]
|
||||||
|
pub enum GameDownloadStatus {
|
||||||
|
Remote {},
|
||||||
|
SetupRequired {
|
||||||
|
version_name: String,
|
||||||
|
install_dir: String,
|
||||||
|
},
|
||||||
|
Installed {
|
||||||
|
version_name: String,
|
||||||
|
install_dir: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stuff that shouldn't be synced to disk
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub enum ApplicationTransientStatus {
|
||||||
|
Downloading { version_name: String },
|
||||||
|
Uninstalling {},
|
||||||
|
Updating { version_name: String },
|
||||||
|
Running {},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone, Deserialize)]
|
||||||
|
#[native_model(id = 6, version = 1)]
|
||||||
|
pub struct DatabaseAuth {
|
||||||
|
pub private: String,
|
||||||
|
pub cert: String,
|
||||||
|
pub client_id: String,
|
||||||
|
pub web_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[native_model(id = 8, version = 1)]
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy,
|
||||||
|
)]
|
||||||
|
pub enum DownloadType {
|
||||||
|
Game,
|
||||||
|
Tool,
|
||||||
|
DLC,
|
||||||
|
Mod,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[native_model(id = 7, version = 1)]
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DownloadableMetadata {
|
||||||
|
pub id: String,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub download_type: DownloadType,
|
||||||
|
}
|
||||||
|
impl DownloadableMetadata {
|
||||||
|
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
download_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[native_model(id = 1, version = 1)]
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct Database {
|
||||||
|
#[serde(default)]
|
||||||
|
pub settings: Settings,
|
||||||
|
pub auth: Option<DatabaseAuth>,
|
||||||
|
pub base_url: String,
|
||||||
|
pub applications: DatabaseApplications,
|
||||||
|
pub prev_database: Option<PathBuf>,
|
||||||
|
pub cache_dir: PathBuf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod v2 {
|
||||||
|
use std::{collections::HashMap, path::PathBuf, process::Command};
|
||||||
|
|
||||||
|
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[native_model(id = 1, version = 2)]
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct Database {
|
||||||
|
#[serde(default)]
|
||||||
|
pub settings: Settings,
|
||||||
|
pub auth: Option<DatabaseAuth>,
|
||||||
|
pub base_url: String,
|
||||||
|
pub applications: DatabaseApplications,
|
||||||
|
pub prev_database: Option<PathBuf>,
|
||||||
|
pub cache_dir: PathBuf,
|
||||||
|
pub compat_info: Option<DatabaseCompatInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[native_model(id = 8, version = 2)]
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
|
||||||
|
pub struct DatabaseCompatInfo {
|
||||||
|
umu_installed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
return None;
|
||||||
|
|
||||||
|
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
|
||||||
|
Some(DatabaseCompatInfo {
|
||||||
|
umu_installed: has_umu_installed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new<T: Into<PathBuf>>(
|
||||||
|
games_base_dir: T,
|
||||||
|
prev_database: Option<PathBuf>,
|
||||||
|
cache_dir: PathBuf,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
applications: DatabaseApplications {
|
||||||
|
install_dirs: vec![games_base_dir.into()],
|
||||||
|
game_statuses: HashMap::new(),
|
||||||
|
game_versions: HashMap::new(),
|
||||||
|
installed_game_version: HashMap::new(),
|
||||||
|
transient_statuses: HashMap::new(),
|
||||||
|
},
|
||||||
|
prev_database,
|
||||||
|
base_url: "".to_owned(),
|
||||||
|
auth: None,
|
||||||
|
settings: Settings::default(),
|
||||||
|
cache_dir,
|
||||||
|
compat_info: Database::create_new_compat_info(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<v1::Database> for Database {
|
||||||
|
fn from(value: v1::Database) -> Self {
|
||||||
|
Self {
|
||||||
|
settings: value.settings,
|
||||||
|
auth: value.auth,
|
||||||
|
base_url: value.base_url,
|
||||||
|
applications: value.applications,
|
||||||
|
prev_database: value.prev_database,
|
||||||
|
cache_dir: value.cache_dir,
|
||||||
|
compat_info: Database::create_new_compat_info(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Settings {
|
|
||||||
pub autostart: bool,
|
|
||||||
pub max_download_threads: usize,
|
|
||||||
pub force_offline: bool
|
|
||||||
// ... other settings ...
|
|
||||||
}
|
|
||||||
impl Default for Settings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
autostart: false,
|
|
||||||
max_download_threads: 4,
|
|
||||||
force_offline: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ideally use pointers instead of a macro to assign the settings
|
|
||||||
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
|
|
||||||
// where T: for<'a> Deserialize<'a>
|
|
||||||
// {
|
|
||||||
// *t = serde_json::from_value(v)?;
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
|
use crate::{database::models::data::DownloadableMetadata, AppState};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
|
||||||
|
|||||||
@ -12,12 +12,13 @@ use std::{
|
|||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::{
|
||||||
|
database::models::data::DownloadableMetadata,
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager_builder::{CurrentProgressObject, DownloadAgent},
|
download_manager_builder::{CurrentProgressObject, DownloadAgent}, util::queue::Queue,
|
||||||
downloadable_metadata::DownloadableMetadata,
|
|
||||||
queue::Queue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum DownloadManagerSignal {
|
pub enum DownloadManagerSignal {
|
||||||
@ -167,10 +168,7 @@ impl DownloadManager {
|
|||||||
self.command_sender
|
self.command_sender
|
||||||
.send(DownloadManagerSignal::UpdateUIQueue)
|
.send(DownloadManagerSignal::UpdateUIQueue)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.command_sender
|
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
|
||||||
.send(DownloadManagerSignal::Go)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
pub fn pause_downloads(&self) {
|
pub fn pause_downloads(&self) {
|
||||||
self.command_sender
|
self.command_sender
|
||||||
|
|||||||
@ -11,17 +11,14 @@ use log::{debug, error, info, warn};
|
|||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
database::models::data::DownloadableMetadata,
|
||||||
error::application_download_error::ApplicationDownloadError,
|
error::application_download_error::ApplicationDownloadError,
|
||||||
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
|
||||||
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
|
downloadable::Downloadable, util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue},
|
||||||
downloadable::Downloadable,
|
|
||||||
downloadable_metadata::DownloadableMetadata,
|
|
||||||
progress_object::ProgressObject,
|
|
||||||
queue::Queue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
|
||||||
@ -209,7 +206,9 @@ impl DownloadManagerBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.current_download_agent.is_some() {
|
if self.current_download_agent.is_some() {
|
||||||
if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() {
|
if self.download_queue.read().front().unwrap()
|
||||||
|
== &self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
|
{
|
||||||
debug!(
|
debug!(
|
||||||
"Current download agent: {:?}",
|
"Current download agent: {:?}",
|
||||||
self.current_download_agent.as_ref().unwrap().metadata()
|
self.current_download_agent.as_ref().unwrap().metadata()
|
||||||
|
|||||||
@ -2,11 +2,13 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::{
|
||||||
|
database::models::data::DownloadableMetadata,
|
||||||
|
error::application_download_error::ApplicationDownloadError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
|
download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
|
||||||
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait Downloadable: Send + Sync {
|
pub trait Downloadable: Send + Sync {
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
|
|
||||||
pub enum DownloadType {
|
|
||||||
Game,
|
|
||||||
Tool,
|
|
||||||
DLC,
|
|
||||||
Mod,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DownloadableMetadata {
|
|
||||||
pub id: String,
|
|
||||||
pub version: Option<String>,
|
|
||||||
pub download_type: DownloadType,
|
|
||||||
}
|
|
||||||
impl DownloadableMetadata {
|
|
||||||
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
version,
|
|
||||||
download_type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
use std::{fmt::Display, io, sync::mpsc::SendError};
|
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
|
||||||
|
|
||||||
#[derive(SerializeDisplay)]
|
|
||||||
pub enum InternalError<T> {
|
|
||||||
IOError(io::Error),
|
|
||||||
SignalError(SendError<T>),
|
|
||||||
}
|
|
||||||
impl<T> Display for InternalError<T> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
InternalError::IOError(error) => write!(f, "{}", error),
|
|
||||||
InternalError::SignalError(send_error) => write!(f, "{}", send_error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> From<SendError<T>> for InternalError<T> {
|
|
||||||
fn from(value: SendError<T>) -> Self {
|
|
||||||
InternalError::SignalError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> From<io::Error> for InternalError<T> {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
InternalError::IOError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,5 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod download_manager;
|
pub mod download_manager;
|
||||||
pub mod download_manager_builder;
|
pub mod download_manager_builder;
|
||||||
pub mod download_thread_control_flag;
|
|
||||||
pub mod downloadable;
|
pub mod downloadable;
|
||||||
pub mod downloadable_metadata;
|
pub mod util;
|
||||||
pub mod internal_error;
|
|
||||||
pub mod progress_object;
|
|
||||||
pub mod queue;
|
|
||||||
pub mod rolling_progress_updates;
|
|
||||||
4
src-tauri/src/download_manager/util/mod.rs
Normal file
4
src-tauri/src/download_manager/util/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod progress_object;
|
||||||
|
pub mod queue;
|
||||||
|
pub mod rolling_progress_updates;
|
||||||
|
pub mod download_thread_control_flag;
|
||||||
@ -10,8 +10,10 @@ 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::download_manager::DownloadManagerSignal;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
|
rolling_progress_updates::RollingProgressWindow,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -3,7 +3,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex, MutexGuard},
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::downloadable_metadata::DownloadableMetadata;
|
use crate::database::models::data::DownloadableMetadata;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
@ -1,21 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
|
||||||
|
|
||||||
#[derive(Debug, SerializeDisplay, Clone, Copy)]
|
|
||||||
pub enum BackupError {
|
|
||||||
InvalidSystem,
|
|
||||||
NotFound,
|
|
||||||
ParseError
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for BackupError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let s = match self {
|
|
||||||
BackupError::InvalidSystem => "Attempted to generate path for invalid system",
|
|
||||||
BackupError::NotFound => "Could not generate or find path",
|
|
||||||
BackupError::ParseError => "Failed to parse path",
|
|
||||||
};
|
|
||||||
write!(f, "{}", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
src-tauri/src/error/download_manager_error.rs
Normal file
27
src-tauri/src/error/download_manager_error.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use std::{fmt::Display, io, sync::mpsc::SendError};
|
||||||
|
|
||||||
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
|
#[derive(SerializeDisplay)]
|
||||||
|
pub enum DownloadManagerError<T> {
|
||||||
|
IOError(io::Error),
|
||||||
|
SignalError(SendError<T>),
|
||||||
|
}
|
||||||
|
impl<T> Display for DownloadManagerError<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DownloadManagerError::IOError(error) => write!(f, "{}", error),
|
||||||
|
DownloadManagerError::SignalError(send_error) => write!(f, "{}", send_error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<SendError<T>> for DownloadManagerError<T> {
|
||||||
|
fn from(value: SendError<T>) -> Self {
|
||||||
|
DownloadManagerError::SignalError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<io::Error> for DownloadManagerError<T> {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
DownloadManagerError::IOError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
pub mod application_download_error;
|
pub mod application_download_error;
|
||||||
pub mod drop_server_error;
|
pub mod drop_server_error;
|
||||||
|
pub mod download_manager_error;
|
||||||
pub mod library_error;
|
pub mod library_error;
|
||||||
pub mod process_error;
|
pub mod process_error;
|
||||||
pub mod remote_access_error;
|
pub mod remote_access_error;
|
||||||
pub mod setup_error;
|
pub mod setup_error;
|
||||||
pub mod backup_error;
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{Display, Formatter},
|
fmt::{Display, Formatter},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -23,7 +22,6 @@ pub enum RemoteAccessError {
|
|||||||
ManifestDownloadFailed(StatusCode, String),
|
ManifestDownloadFailed(StatusCode, String),
|
||||||
OutOfSync,
|
OutOfSync,
|
||||||
Cache(cacache::Error),
|
Cache(cacache::Error),
|
||||||
Generic(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for RemoteAccessError {
|
impl Display for RemoteAccessError {
|
||||||
@ -59,7 +57,6 @@ impl Display for RemoteAccessError {
|
|||||||
status, response
|
status, response
|
||||||
),
|
),
|
||||||
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
|
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
|
||||||
RemoteAccessError::Generic(message) => write!(f, "{}", message),
|
|
||||||
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
|
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {}", error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ pub struct Collection {
|
|||||||
name: String,
|
name: String,
|
||||||
is_default: bool,
|
is_default: bool,
|
||||||
user_id: String,
|
user_id: String,
|
||||||
entries: Vec<CollectionObject>
|
entries: Vec<CollectionObject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
@ -21,4 +21,3 @@ pub struct CollectionObject {
|
|||||||
game_id: String,
|
game_id: String,
|
||||||
game: Game,
|
game: Game,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,12 @@ use reqwest::blocking::Client;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, DB};
|
use crate::{
|
||||||
|
database::db::DatabaseImpls,
|
||||||
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
remote::{auth::generate_authorization_header, requests::make_request},
|
||||||
|
DB,
|
||||||
|
};
|
||||||
|
|
||||||
use super::collection::{Collection, Collections};
|
use super::collection::{Collection, Collections};
|
||||||
|
|
||||||
@ -20,9 +25,12 @@ pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let response = make_request(&client, &["/api/v1/client/collection/", &collection_id], &[], |r| {
|
let response = make_request(
|
||||||
r.header("Authorization", generate_authorization_header())
|
&client,
|
||||||
})?
|
&["/api/v1/client/collection/", &collection_id],
|
||||||
|
&[],
|
||||||
|
|r| r.header("Authorization", generate_authorization_header()),
|
||||||
|
)?
|
||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
Ok(response.json()?)
|
Ok(response.json()?)
|
||||||
@ -35,7 +43,6 @@ pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError>
|
|||||||
|
|
||||||
let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?;
|
let base_url = Url::parse(&format!("{}api/v1/client/collection/", base_url))?;
|
||||||
|
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.post(base_url)
|
.post(base_url)
|
||||||
.header("Authorization", generate_authorization_header())
|
.header("Authorization", generate_authorization_header())
|
||||||
@ -46,9 +53,16 @@ pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_game_to_collection(collection_id: String, game_id: String) -> Result<(), RemoteAccessError> {
|
pub fn add_game_to_collection(
|
||||||
|
collection_id: String,
|
||||||
|
game_id: String,
|
||||||
|
) -> Result<(), RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let url = Url::parse(&format!("{}api/v1/client/collection/{}/entry/", DB.fetch_base_url(), collection_id))?;
|
let url = Url::parse(&format!(
|
||||||
|
"{}api/v1/client/collection/{}/entry/",
|
||||||
|
DB.fetch_base_url(),
|
||||||
|
collection_id
|
||||||
|
))?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.post(url)
|
.post(url)
|
||||||
@ -61,7 +75,11 @@ pub fn add_game_to_collection(collection_id: String, game_id: String) -> Result<
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let base_url = Url::parse(&format!("{}api/v1/client/collection/{}", DB.fetch_base_url(), collection_id))?;
|
let base_url = Url::parse(&format!(
|
||||||
|
"{}api/v1/client/collection/{}",
|
||||||
|
DB.fetch_base_url(),
|
||||||
|
collection_id
|
||||||
|
))?;
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.delete(base_url)
|
.delete(base_url)
|
||||||
@ -71,9 +89,16 @@ pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessErro
|
|||||||
Ok(response.json()?)
|
Ok(response.json()?)
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_game_in_collection(collection_id: String, game_id: String) -> Result<(), RemoteAccessError> {
|
pub fn delete_game_in_collection(
|
||||||
|
collection_id: String,
|
||||||
|
game_id: String,
|
||||||
|
) -> Result<(), RemoteAccessError> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let base_url = Url::parse(&format!("{}api/v1/client/collection/{}/entry", DB.fetch_base_url(), collection_id))?;
|
let base_url = Url::parse(&format!(
|
||||||
|
"{}api/v1/client/collection/{}/entry",
|
||||||
|
DB.fetch_base_url(),
|
||||||
|
collection_id
|
||||||
|
))?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.delete(base_url)
|
.delete(base_url)
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
pub mod commands;
|
|
||||||
pub mod collection;
|
pub mod collection;
|
||||||
|
pub mod commands;
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta, uninstall_game_logic}, offline, AppState
|
database::models::data::GameVersion,
|
||||||
|
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
|
||||||
|
games::library::{
|
||||||
|
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
|
||||||
|
uninstall_game_logic,
|
||||||
|
},
|
||||||
|
offline, AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -15,16 +21,29 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_library(state: tauri::State<'_, Mutex<AppState>>) -> Result<Vec<Game>, RemoteAccessError> {
|
pub fn fetch_library(
|
||||||
offline!(state, fetch_library_logic, fetch_library_logic_offline, state)
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<Vec<Game>, RemoteAccessError> {
|
||||||
|
offline!(
|
||||||
|
state,
|
||||||
|
fetch_library_logic,
|
||||||
|
fetch_library_logic_offline,
|
||||||
|
state
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_game(
|
pub fn fetch_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<FetchGameStruct, RemoteAccessError> {
|
) -> Result<FetchGameStruct, RemoteAccessError> {
|
||||||
offline!(state, fetch_game_logic, fetch_game_logic_offline, game_id, state)
|
offline!(
|
||||||
|
state,
|
||||||
|
fetch_game_logic,
|
||||||
|
fetch_game_logic_offline,
|
||||||
|
game_id,
|
||||||
|
state
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@ -3,9 +3,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use crate::{
|
use crate::{
|
||||||
download_manager::{
|
download_manager::{
|
||||||
download_manager::DownloadManagerSignal, downloadable::Downloadable,
|
download_manager::DownloadManagerSignal, downloadable::Downloadable,
|
||||||
internal_error::InternalError,
|
}, error::download_manager_error::DownloadManagerError, AppState
|
||||||
},
|
|
||||||
AppState,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::download_agent::GameDownloadAgent;
|
use super::download_agent::GameDownloadAgent;
|
||||||
@ -16,7 +14,7 @@ pub fn download_game(
|
|||||||
game_version: String,
|
game_version: String,
|
||||||
install_dir: usize,
|
install_dir: usize,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), InternalError<DownloadManagerSignal>> {
|
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
|
||||||
let sender = state.lock().unwrap().download_manager.get_sender();
|
let sender = state.lock().unwrap().download_manager.get_sender();
|
||||||
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
|
||||||
game_id,
|
game_id,
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
use crate::auth::generate_authorization_header;
|
use crate::auth::generate_authorization_header;
|
||||||
use crate::database::db::{
|
use crate::database::db::borrow_db_checked;
|
||||||
borrow_db_checked, ApplicationTransientStatus, DatabaseImpls,
|
use crate::database::models::data::{
|
||||||
GameDownloadStatus,
|
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||||
};
|
};
|
||||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||||
use crate::download_manager::download_thread_control_flag::{
|
|
||||||
DownloadThreadControl, DownloadThreadControlFlag,
|
|
||||||
};
|
|
||||||
use crate::download_manager::downloadable::Downloadable;
|
use crate::download_manager::downloadable::Downloadable;
|
||||||
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
|
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||||
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
|
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||||
@ -25,7 +22,6 @@ use std::sync::mpsc::Sender;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use urlencoding::encode;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use rustix::fs::{fallocate, FallocateFlags};
|
use rustix::fs::{fallocate, FallocateFlags};
|
||||||
@ -377,7 +373,10 @@ impl Downloadable for GameDownloadAgent {
|
|||||||
error!("error while managing download: {}", error);
|
error!("error while managing download: {}", error);
|
||||||
|
|
||||||
let mut handle = DB.borrow_data_mut().unwrap();
|
let mut handle = DB.borrow_data_mut().unwrap();
|
||||||
handle.applications.transient_statuses.remove(&self.metadata());
|
handle
|
||||||
|
.applications
|
||||||
|
.transient_statuses
|
||||||
|
.remove(&self.metadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
fn on_complete(&self, app_handle: &tauri::AppHandle) {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
use crate::download_manager::download_thread_control_flag::{
|
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
|
||||||
DownloadThreadControl, DownloadThreadControlFlag,
|
use crate::download_manager::util::progress_object::ProgressHandle;
|
||||||
};
|
|
||||||
use crate::download_manager::progress_object::ProgressHandle;
|
|
||||||
use crate::error::application_download_error::ApplicationDownloadError;
|
use crate::error::application_download_error::ApplicationDownloadError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::downloads::manifest::DropDownloadContext;
|
use crate::games::downloads::manifest::DropDownloadContext;
|
||||||
|
|||||||
@ -2,15 +2,16 @@ use std::fs::remove_dir_all;
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::thread::spawn;
|
use std::thread::spawn;
|
||||||
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
|
|
||||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
|
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||||
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
|
use crate::database::models::data::{
|
||||||
|
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
|
||||||
|
};
|
||||||
use crate::download_manager::download_manager::DownloadStatus;
|
use crate::download_manager::download_manager::DownloadStatus;
|
||||||
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
|
|
||||||
use crate::error::library_error::LibraryError;
|
use crate::error::library_error::LibraryError;
|
||||||
use crate::error::remote_access_error::RemoteAccessError;
|
use crate::error::remote_access_error::RemoteAccessError;
|
||||||
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
|
||||||
@ -35,11 +36,11 @@ pub struct Game {
|
|||||||
m_description: String,
|
m_description: String,
|
||||||
// mDevelopers
|
// mDevelopers
|
||||||
// mPublishers
|
// mPublishers
|
||||||
m_icon_id: String,
|
m_icon_object_id: String,
|
||||||
m_banner_id: String,
|
m_banner_object_id: String,
|
||||||
m_cover_id: String,
|
m_cover_object_id: String,
|
||||||
m_image_library: Vec<String>,
|
m_image_library_object_ids: Vec<String>,
|
||||||
m_image_carousel: Vec<String>,
|
m_image_carousel_object_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
#[derive(serde::Serialize, Clone)]
|
#[derive(serde::Serialize, Clone)]
|
||||||
pub struct GameUpdateEvent {
|
pub struct GameUpdateEvent {
|
||||||
@ -438,7 +439,12 @@ pub fn on_game_complete(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, version: Option<GameVersion>, status: GameStatusWithTransient) {
|
pub fn push_game_update(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
game_id: &String,
|
||||||
|
version: Option<GameVersion>,
|
||||||
|
status: GameStatusWithTransient,
|
||||||
|
) {
|
||||||
app_handle
|
app_handle
|
||||||
.emit(
|
.emit(
|
||||||
&format!("update_game/{}", game_id),
|
&format!("update_game/{}", game_id),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
pub mod collections;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod collections;
|
|
||||||
@ -1,4 +1,7 @@
|
|||||||
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
|
use crate::database::{
|
||||||
|
db::borrow_db_checked,
|
||||||
|
models::data::{ApplicationTransientStatus, GameDownloadStatus},
|
||||||
|
};
|
||||||
|
|
||||||
pub type GameStatusWithTransient = (
|
pub type GameStatusWithTransient = (
|
||||||
Option<GameDownloadStatus>,
|
Option<GameDownloadStatus>,
|
||||||
|
|||||||
@ -1,27 +1,24 @@
|
|||||||
mod database;
|
mod database;
|
||||||
mod games;
|
mod games;
|
||||||
|
|
||||||
mod autostart;
|
mod client;
|
||||||
mod cleanup;
|
|
||||||
mod cloud_saves;
|
|
||||||
mod commands;
|
|
||||||
mod download_manager;
|
mod download_manager;
|
||||||
mod error;
|
mod error;
|
||||||
mod process;
|
mod process;
|
||||||
mod remote;
|
mod remote;
|
||||||
|
|
||||||
use crate::database::db::DatabaseImpls;
|
use crate::database::db::DatabaseImpls;
|
||||||
use autostart::{get_autostart_enabled, toggle_autostart};
|
use client::{
|
||||||
use cleanup::{cleanup_and_exit, quit};
|
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
|
||||||
use cloud_saves::resolver::test;
|
cleanup::{cleanup_and_exit, quit},
|
||||||
use commands::fetch_state;
|
};
|
||||||
|
use client::commands::fetch_state;
|
||||||
use database::commands::{
|
use database::commands::{
|
||||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
|
||||||
fetch_system_data, update_settings,
|
fetch_system_data, update_settings,
|
||||||
};
|
};
|
||||||
use database::db::{
|
use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, DATA_ROOT_DIR};
|
||||||
borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
|
use database::models::data::GameDownloadStatus;
|
||||||
};
|
|
||||||
use download_manager::commands::{
|
use download_manager::commands::{
|
||||||
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
||||||
};
|
};
|
||||||
@ -36,8 +33,6 @@ use games::commands::{
|
|||||||
};
|
};
|
||||||
use games::downloads::commands::download_game;
|
use games::downloads::commands::download_game;
|
||||||
use games::library::{update_game_configuration, Game};
|
use games::library::{update_game_configuration, Game};
|
||||||
use http::Response;
|
|
||||||
use http::{header::*, response::Builder as ResponseBuilder};
|
|
||||||
use log::{debug, info, warn, LevelFilter};
|
use log::{debug, info, warn, LevelFilter};
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
@ -52,9 +47,7 @@ use remote::commands::{
|
|||||||
sign_out, use_remote,
|
sign_out, use_remote,
|
||||||
};
|
};
|
||||||
use remote::fetch_object::{fetch_object, fetch_object_offline};
|
use remote::fetch_object::{fetch_object, fetch_object_offline};
|
||||||
use remote::requests::make_request;
|
|
||||||
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
use remote::server_proto::{handle_server_proto, handle_server_proto_offline};
|
||||||
use reqwest::blocking::Body;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@ -88,10 +81,9 @@ pub struct User {
|
|||||||
username: String,
|
username: String,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
profile_picture: String,
|
profile_picture_object_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppState<'a> {
|
pub struct AppState<'a> {
|
||||||
@ -106,7 +98,6 @@ pub struct AppState<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup(handle: AppHandle) -> AppState<'static> {
|
fn setup(handle: AppHandle) -> AppState<'static> {
|
||||||
test();
|
|
||||||
let logfile = FileAppender::builder()
|
let logfile = FileAppender::builder()
|
||||||
.encoder(Box::new(PatternEncoder::new(
|
.encoder(Box::new(PatternEncoder::new(
|
||||||
"{d} | {l} | {f}:{L} - {m}{n}",
|
"{d} | {l} | {f}:{L} - {m}{n}",
|
||||||
@ -164,8 +155,8 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
drop(db_handle);
|
drop(db_handle);
|
||||||
for (game_id, status) in statuses.into_iter() {
|
for (game_id, status) in statuses.into_iter() {
|
||||||
match status {
|
match status {
|
||||||
database::db::GameDownloadStatus::Remote {} => {}
|
GameDownloadStatus::Remote {} => {}
|
||||||
database::db::GameDownloadStatus::SetupRequired {
|
GameDownloadStatus::SetupRequired {
|
||||||
version_name: _,
|
version_name: _,
|
||||||
install_dir,
|
install_dir,
|
||||||
} => {
|
} => {
|
||||||
@ -174,7 +165,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
missing_games.push(game_id);
|
missing_games.push(game_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
database::db::GameDownloadStatus::Installed {
|
GameDownloadStatus::Installed {
|
||||||
version_name: _,
|
version_name: _,
|
||||||
install_dir,
|
install_dir,
|
||||||
} => {
|
} => {
|
||||||
@ -202,7 +193,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
|||||||
debug!("finished setup!");
|
debug!("finished setup!");
|
||||||
|
|
||||||
// Sync autostart state
|
// Sync autostart state
|
||||||
if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
|
if let Err(e) = sync_autostart_on_startup(&handle) {
|
||||||
warn!("failed to sync autostart state: {}", e);
|
warn!("failed to sync autostart state: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,10 +282,9 @@ pub fn run() {
|
|||||||
debug!("initialized drop client");
|
debug!("initialized drop client");
|
||||||
app.manage(Mutex::new(state));
|
app.manage(Mutex::new(state));
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
|
||||||
{
|
{
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
app.deep_link().register_all()?;
|
let _ = app.deep_link().register_all();
|
||||||
debug!("registered all pre-defined deep links");
|
debug!("registered all pre-defined deep links");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,6 +327,7 @@ pub fn run() {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
run_on_tray(|| {
|
||||||
TrayIconBuilder::new()
|
TrayIconBuilder::new()
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
@ -354,6 +345,7 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.build(app)
|
.build(app)
|
||||||
.expect("error while setting up tray menu");
|
.expect("error while setting up tray menu");
|
||||||
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db_handle = borrow_db_mut_checked();
|
let mut db_handle = borrow_db_mut_checked();
|
||||||
@ -397,12 +389,13 @@ pub fn run() {
|
|||||||
request,
|
request,
|
||||||
responder
|
responder
|
||||||
);
|
);
|
||||||
|
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
run_on_tray(|| {
|
||||||
window.hide().unwrap();
|
window.hide().unwrap();
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
@ -410,9 +403,20 @@ pub fn run() {
|
|||||||
|
|
||||||
app.run(|_app_handle, event| {
|
app.run(|_app_handle, event| {
|
||||||
if let RunEvent::ExitRequested { code, api, .. } = event {
|
if let RunEvent::ExitRequested { code, api, .. } = event {
|
||||||
|
run_on_tray(|| {
|
||||||
if code.is_none() {
|
if code.is_none() {
|
||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_on_tray<T: FnOnce() -> ()>(f: T) {
|
||||||
|
if match std::env::var("NO_TRAY_ICON") {
|
||||||
|
Ok(s) => s.to_lowercase() != "true",
|
||||||
|
Err(_) => true,
|
||||||
|
} {
|
||||||
|
(f)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1 @@
|
|||||||
// Since this code isn't being used, we can either:
|
|
||||||
// 1. Delete the entire file if compatibility features are not planned
|
|
||||||
// 2. Or add a TODO comment if planning to implement later
|
|
||||||
|
|
||||||
// Option 1: Delete the file
|
|
||||||
// Delete src-tauri/src/process/compat.rs
|
|
||||||
|
|
||||||
// Option 2: Add TODO comment
|
|
||||||
/*
|
|
||||||
TODO: Compatibility layer for running Windows games on Linux
|
|
||||||
This module is currently unused but reserved for future implementation
|
|
||||||
of Windows game compatibility features on Linux.
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub mod compat;
|
pub mod compat;
|
||||||
pub mod process_manager;
|
pub mod process_manager;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::{File, OpenOptions},
|
fs::OpenOptions,
|
||||||
io::{self, Error},
|
io::{self},
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
process::{Child, Command, ExitStatus},
|
process::{Command, ExitStatus},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread::spawn,
|
thread::spawn,
|
||||||
@ -15,14 +15,15 @@ use log::{debug, info, warn};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use shared_child::SharedChild;
|
use shared_child::SharedChild;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::{
|
database::{
|
||||||
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion,
|
db::{borrow_db_mut_checked, DATA_ROOT_DIR},
|
||||||
DATA_ROOT_DIR,
|
models::data::{
|
||||||
|
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus,
|
||||||
|
GameVersion,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
|
|
||||||
error::process_error::ProcessError,
|
error::process_error::ProcessError,
|
||||||
games::{library::push_game_update, state::GameStatusManager},
|
games::{library::push_game_update, state::GameStatusManager},
|
||||||
AppState, DB,
|
AppState, DB,
|
||||||
@ -266,10 +267,13 @@ impl ProcessManager<'_> {
|
|||||||
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
.map_err(|e| ProcessError::FormatError(e.to_string()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let mut command = Command::new("cmd");
|
let mut command = Command::new("cmd");
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
command.args(["/C", &launch_string]);
|
command.raw_arg(format!("/C \"{}\"", &launch_string));
|
||||||
|
|
||||||
info!("launching (in {}): {}", install_dir, launch_string,);
|
info!("launching (in {}): {}", install_dir, launch_string,);
|
||||||
|
|
||||||
@ -324,56 +328,13 @@ impl ProcessManager<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
|
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum Platform {
|
pub enum Platform {
|
||||||
Windows,
|
Windows,
|
||||||
Linux,
|
Linux,
|
||||||
MacOs,
|
MacOs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Platform {
|
|
||||||
const WINDOWS: bool = cfg!(target_os = "windows");
|
|
||||||
const MAC: bool = cfg!(target_os = "macos");
|
|
||||||
const LINUX: bool = cfg!(target_os = "linux");
|
|
||||||
#[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,
|
||||||
@ -392,14 +353,14 @@ impl ProcessHandler for NativeGameLauncher {
|
|||||||
_meta: &DownloadableMetadata,
|
_meta: &DownloadableMetadata,
|
||||||
launch_command: String,
|
launch_command: String,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
game_version: &GameVersion,
|
_game_version: &GameVersion,
|
||||||
current_dir: &str,
|
_current_dir: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!("\"{}\" {}", launch_command, args.join(" "))
|
format!("\"{}\" {}", launch_command, args.join(" "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
|
||||||
struct UMULauncher;
|
struct UMULauncher;
|
||||||
impl ProcessHandler for UMULauncher {
|
impl ProcessHandler for UMULauncher {
|
||||||
fn create_launch_process(
|
fn create_launch_process(
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
use std::{env, sync::Mutex};
|
use std::{collections::HashMap, env};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use droplet_rs::ssl::sign_nonce;
|
use droplet_rs::ssl::sign_nonce;
|
||||||
use gethostname::gethostname;
|
use gethostname::gethostname;
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use tauri::{AppHandle, Emitter};
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::{
|
database::{
|
||||||
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
|
db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||||
|
models::data::DatabaseAuth,
|
||||||
},
|
},
|
||||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||||
AppState, AppStatus, User, DB,
|
AppStatus, User,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -22,11 +22,16 @@ use super::{
|
|||||||
requests::make_request,
|
requests::make_request,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CapabilityConfiguration {}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct InitiateRequestBody {
|
struct InitiateRequestBody {
|
||||||
name: String,
|
name: String,
|
||||||
platform: String,
|
platform: String,
|
||||||
|
capabilities: HashMap<String, CapabilityConfiguration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -58,8 +63,6 @@ pub fn generate_authorization_header() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||||
let base_url = DB.fetch_base_url();
|
|
||||||
|
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
@ -106,6 +109,9 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = client.post(endpoint).json(&body).send()?;
|
let response = client.post(endpoint).json(&body).send()?;
|
||||||
debug!("handshake responsded with {}", response.status().as_u16());
|
debug!("handshake responsded with {}", response.status().as_u16());
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(RemoteAccessError::InvalidResponse(response.json()?));
|
||||||
|
}
|
||||||
let response_struct: HandshakeResponse = response.json()?;
|
let response_struct: HandshakeResponse = response.json()?;
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -137,30 +143,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||||||
drop(handle);
|
drop(handle);
|
||||||
save_db();
|
save_db();
|
||||||
|
|
||||||
{
|
|
||||||
let app_state = app.state::<Mutex<AppState>>();
|
|
||||||
let mut app_state_handle = app_state.lock().unwrap();
|
|
||||||
app_state_handle.status = AppStatus::SignedIn;
|
|
||||||
app_state_handle.user = Some(fetch_user()?);
|
|
||||||
|
|
||||||
// Setup capabilities
|
|
||||||
let endpoint = base_url.join("/api/v1/client/capability")?;
|
|
||||||
let header = generate_authorization_header();
|
|
||||||
let body = json!({
|
|
||||||
"capability": "cloudSaves",
|
|
||||||
"configuration": {}
|
|
||||||
});
|
|
||||||
let response = client
|
|
||||||
.post(endpoint)
|
|
||||||
.header("Authorization", header)
|
|
||||||
.json(&body)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
debug!("registered client for 'cloudSaves' capability")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +172,10 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
|||||||
let body = InitiateRequestBody {
|
let body = InitiateRequestBody {
|
||||||
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
|
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
|
||||||
platform: env::consts::OS.to_string(),
|
platform: env::consts::OS.to_string(),
|
||||||
|
capabilities: HashMap::from([
|
||||||
|
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||||
|
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
use std::sync::RwLockReadGuard;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::{borrow_db_checked, Database},
|
database::{db::borrow_db_checked, models::data::Database},
|
||||||
error::remote_access_error::RemoteAccessError,
|
error::remote_access_error::RemoteAccessError,
|
||||||
};
|
};
|
||||||
use cacache::Integrity;
|
use cacache::Integrity;
|
||||||
@ -9,7 +7,6 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response}
|
|||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use serde_binary::binary_stream::Endian;
|
use serde_binary::binary_stream::Endian;
|
||||||
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! offline {
|
macro_rules! offline {
|
||||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||||
|
|||||||
@ -6,11 +6,16 @@ use tauri::{AppHandle, Emitter, Manager};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, AppState, AppStatus
|
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
|
||||||
|
error::remote_access_error::RemoteAccessError,
|
||||||
|
remote::{auth::generate_authorization_header, requests::make_request},
|
||||||
|
AppState, AppStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
auth::{auth_initiate_logic, recieve_handshake, setup}, cache::{cache_object, get_cached_object}, remote::use_remote_logic
|
auth::{auth_initiate_logic, recieve_handshake, setup},
|
||||||
|
cache::{cache_object, get_cached_object},
|
||||||
|
remote::use_remote_logic,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -36,24 +41,22 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
|
||||||
let drop_url = gen_drop_url(path.clone());
|
let _drop_url = gen_drop_url(path.clone())?;
|
||||||
let req = make_request(
|
let req = make_request(&Client::new(), &[&path], &[], |r| {
|
||||||
&Client::new(),
|
r.header("Authorization", generate_authorization_header())
|
||||||
&[&path],
|
})?
|
||||||
&[],
|
.send();
|
||||||
|r| { r.header("Authorization", generate_authorization_header()) }
|
|
||||||
)?.send();
|
|
||||||
|
|
||||||
match req {
|
match req {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let data = data.bytes()?.to_vec();
|
let data = data.bytes()?.to_vec();
|
||||||
cache_object(&path, &data)?;
|
cache_object(&path, &data)?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("{}", e);
|
debug!("{}", e);
|
||||||
get_cached_object::<&str, Vec<u8>>(&path)
|
get_cached_object::<&str, Vec<u8>>(&path)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@ -2,12 +2,13 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
use super::{auth::generate_authorization_header, cache::{cache_object, get_cached_object, ObjectCache}, requests::make_request};
|
use super::{
|
||||||
|
auth::generate_authorization_header,
|
||||||
|
cache::{cache_object, get_cached_object, ObjectCache},
|
||||||
|
requests::make_request,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn fetch_object(
|
pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
request: http::Request<Vec<u8>>,
|
|
||||||
responder: UriSchemeResponder,
|
|
||||||
) {
|
|
||||||
// Drop leading /
|
// Drop leading /
|
||||||
let object_id = &request.uri().path()[1..];
|
let object_id = &request.uri().path()[1..];
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ pub fn fetch_object(
|
|||||||
Ok(data) => responder.respond(data.into()),
|
Ok(data) => responder.respond(data.into()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("{}", e)
|
warn!("{}", e)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,10 +42,7 @@ pub fn fetch_object(
|
|||||||
|
|
||||||
responder.respond(resp);
|
responder.respond(resp);
|
||||||
}
|
}
|
||||||
pub fn fetch_object_offline(
|
pub fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||||
request: http::Request<Vec<u8>>,
|
|
||||||
responder: UriSchemeResponder,
|
|
||||||
) {
|
|
||||||
let object_id = &request.uri().path()[1..];
|
let object_id = &request.uri().path()[1..];
|
||||||
let data = get_cached_object::<&str, ObjectCache>(object_id);
|
let data = get_cached_object::<&str, ObjectCache>(object_id);
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
use std::{path::PathBuf, str::FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use http::{
|
use http::{
|
||||||
uri::{Authority, PathAndQuery},
|
uri::PathAndQuery,
|
||||||
Request, Response, StatusCode, Uri,
|
Request, Response, StatusCode, Uri,
|
||||||
};
|
};
|
||||||
use log::info;
|
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use tauri::UriSchemeResponder;
|
use tauri::UriSchemeResponder;
|
||||||
|
|
||||||
|
|||||||
7
src-tauri/tailscale/.gitignore
vendored
Normal file
7
src-tauri/tailscale/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
294
src-tauri/tailscale/Cargo.lock
generated
Normal file
294
src-tauri/tailscale/Cargo.lock
generated
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "abs-file-macro"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3909701959b9fcb4d065c3cf3044fe77f6b07a85d748be4630f5214e8d7acc1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.71.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.172"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tailscale"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"abs-file-macro",
|
||||||
|
"bindgen",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||||
11
src-tauri/tailscale/Cargo.toml
Normal file
11
src-tauri/tailscale/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "tailscale"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
bindgen = "*"
|
||||||
|
abs-file-macro = "0.1.2"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libc = "0.2.172"
|
||||||
36
src-tauri/tailscale/build.rs
Normal file
36
src-tauri/tailscale/build.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
extern crate bindgen;
|
||||||
|
|
||||||
|
use abs_file_macro::abs_file;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let build_folder = PathBuf::from(abs_file!());
|
||||||
|
let build_folder = build_folder.parent().unwrap();
|
||||||
|
|
||||||
|
let in_path = build_folder.join("libtailscale");
|
||||||
|
let out_path = build_folder.join("src/");
|
||||||
|
|
||||||
|
let mut make_cmd = Command::new("make");
|
||||||
|
make_cmd.arg("c-archive");
|
||||||
|
make_cmd.current_dir(in_path.clone());
|
||||||
|
|
||||||
|
make_cmd.status().expect("Make build failed");
|
||||||
|
|
||||||
|
let bindings = bindgen::Builder::default()
|
||||||
|
.header(in_path.join("libtailscale.h").to_str().unwrap())
|
||||||
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate bindings");
|
||||||
|
|
||||||
|
bindings
|
||||||
|
.write_to_file(out_path.join("bindings.rs"))
|
||||||
|
.expect("Couldn't write bindings!");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=libtailscale/tailscale.go");
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-search=native={}",
|
||||||
|
in_path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
println!("cargo:rustc-link-lib=static={}", "tailscale");
|
||||||
|
}
|
||||||
1
src-tauri/tailscale/libtailscale
Submodule
1
src-tauri/tailscale/libtailscale
Submodule
Submodule src-tauri/tailscale/libtailscale added at 78294ac1d6
363
src-tauri/tailscale/src/bindings.rs
Normal file
363
src-tauri/tailscale/src/bindings.rs
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
/* automatically generated by rust-bindgen 0.71.1 */
|
||||||
|
|
||||||
|
#[derive(PartialEq, Copy, Clone, Hash, Debug, Default)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct __BindgenComplex<T> {
|
||||||
|
pub re: T,
|
||||||
|
pub im: T,
|
||||||
|
}
|
||||||
|
pub const _ERRNO_H: u32 = 1;
|
||||||
|
pub const _FEATURES_H: u32 = 1;
|
||||||
|
pub const _DEFAULT_SOURCE: u32 = 1;
|
||||||
|
pub const __GLIBC_USE_ISOC2Y: u32 = 0;
|
||||||
|
pub const __GLIBC_USE_ISOC23: u32 = 0;
|
||||||
|
pub const __USE_ISOC11: u32 = 1;
|
||||||
|
pub const __USE_ISOC99: u32 = 1;
|
||||||
|
pub const __USE_ISOC95: u32 = 1;
|
||||||
|
pub const __USE_POSIX_IMPLICITLY: u32 = 1;
|
||||||
|
pub const _POSIX_SOURCE: u32 = 1;
|
||||||
|
pub const _POSIX_C_SOURCE: u32 = 200809;
|
||||||
|
pub const __USE_POSIX: u32 = 1;
|
||||||
|
pub const __USE_POSIX2: u32 = 1;
|
||||||
|
pub const __USE_POSIX199309: u32 = 1;
|
||||||
|
pub const __USE_POSIX199506: u32 = 1;
|
||||||
|
pub const __USE_XOPEN2K: u32 = 1;
|
||||||
|
pub const __USE_XOPEN2K8: u32 = 1;
|
||||||
|
pub const _ATFILE_SOURCE: u32 = 1;
|
||||||
|
pub const __WORDSIZE: u32 = 64;
|
||||||
|
pub const __WORDSIZE_TIME64_COMPAT32: u32 = 0;
|
||||||
|
pub const __TIMESIZE: u32 = 64;
|
||||||
|
pub const __USE_TIME_BITS64: u32 = 1;
|
||||||
|
pub const __USE_MISC: u32 = 1;
|
||||||
|
pub const __USE_ATFILE: u32 = 1;
|
||||||
|
pub const __USE_FORTIFY_LEVEL: u32 = 0;
|
||||||
|
pub const __GLIBC_USE_DEPRECATED_GETS: u32 = 0;
|
||||||
|
pub const __GLIBC_USE_DEPRECATED_SCANF: u32 = 0;
|
||||||
|
pub const __GLIBC_USE_C23_STRTOL: u32 = 0;
|
||||||
|
pub const _STDC_PREDEF_H: u32 = 1;
|
||||||
|
pub const __STDC_IEC_559__: u32 = 1;
|
||||||
|
pub const __STDC_IEC_60559_BFP__: u32 = 201404;
|
||||||
|
pub const __STDC_IEC_559_COMPLEX__: u32 = 1;
|
||||||
|
pub const __STDC_IEC_60559_COMPLEX__: u32 = 201404;
|
||||||
|
pub const __STDC_ISO_10646__: u32 = 201706;
|
||||||
|
pub const __GNU_LIBRARY__: u32 = 6;
|
||||||
|
pub const __GLIBC__: u32 = 2;
|
||||||
|
pub const __GLIBC_MINOR__: u32 = 41;
|
||||||
|
pub const _SYS_CDEFS_H: u32 = 1;
|
||||||
|
pub const __glibc_c99_flexarr_available: u32 = 1;
|
||||||
|
pub const __LDOUBLE_REDIRECTS_TO_FLOAT128_ABI: u32 = 0;
|
||||||
|
pub const __HAVE_GENERIC_SELECTION: u32 = 1;
|
||||||
|
pub const _BITS_ERRNO_H: u32 = 1;
|
||||||
|
pub const EPERM: u32 = 1;
|
||||||
|
pub const ENOENT: u32 = 2;
|
||||||
|
pub const ESRCH: u32 = 3;
|
||||||
|
pub const EINTR: u32 = 4;
|
||||||
|
pub const EIO: u32 = 5;
|
||||||
|
pub const ENXIO: u32 = 6;
|
||||||
|
pub const E2BIG: u32 = 7;
|
||||||
|
pub const ENOEXEC: u32 = 8;
|
||||||
|
pub const EBADF: u32 = 9;
|
||||||
|
pub const ECHILD: u32 = 10;
|
||||||
|
pub const EAGAIN: u32 = 11;
|
||||||
|
pub const ENOMEM: u32 = 12;
|
||||||
|
pub const EACCES: u32 = 13;
|
||||||
|
pub const EFAULT: u32 = 14;
|
||||||
|
pub const ENOTBLK: u32 = 15;
|
||||||
|
pub const EBUSY: u32 = 16;
|
||||||
|
pub const EEXIST: u32 = 17;
|
||||||
|
pub const EXDEV: u32 = 18;
|
||||||
|
pub const ENODEV: u32 = 19;
|
||||||
|
pub const ENOTDIR: u32 = 20;
|
||||||
|
pub const EISDIR: u32 = 21;
|
||||||
|
pub const EINVAL: u32 = 22;
|
||||||
|
pub const ENFILE: u32 = 23;
|
||||||
|
pub const EMFILE: u32 = 24;
|
||||||
|
pub const ENOTTY: u32 = 25;
|
||||||
|
pub const ETXTBSY: u32 = 26;
|
||||||
|
pub const EFBIG: u32 = 27;
|
||||||
|
pub const ENOSPC: u32 = 28;
|
||||||
|
pub const ESPIPE: u32 = 29;
|
||||||
|
pub const EROFS: u32 = 30;
|
||||||
|
pub const EMLINK: u32 = 31;
|
||||||
|
pub const EPIPE: u32 = 32;
|
||||||
|
pub const EDOM: u32 = 33;
|
||||||
|
pub const ERANGE: u32 = 34;
|
||||||
|
pub const EDEADLK: u32 = 35;
|
||||||
|
pub const ENAMETOOLONG: u32 = 36;
|
||||||
|
pub const ENOLCK: u32 = 37;
|
||||||
|
pub const ENOSYS: u32 = 38;
|
||||||
|
pub const ENOTEMPTY: u32 = 39;
|
||||||
|
pub const ELOOP: u32 = 40;
|
||||||
|
pub const EWOULDBLOCK: u32 = 11;
|
||||||
|
pub const ENOMSG: u32 = 42;
|
||||||
|
pub const EIDRM: u32 = 43;
|
||||||
|
pub const ECHRNG: u32 = 44;
|
||||||
|
pub const EL2NSYNC: u32 = 45;
|
||||||
|
pub const EL3HLT: u32 = 46;
|
||||||
|
pub const EL3RST: u32 = 47;
|
||||||
|
pub const ELNRNG: u32 = 48;
|
||||||
|
pub const EUNATCH: u32 = 49;
|
||||||
|
pub const ENOCSI: u32 = 50;
|
||||||
|
pub const EL2HLT: u32 = 51;
|
||||||
|
pub const EBADE: u32 = 52;
|
||||||
|
pub const EBADR: u32 = 53;
|
||||||
|
pub const EXFULL: u32 = 54;
|
||||||
|
pub const ENOANO: u32 = 55;
|
||||||
|
pub const EBADRQC: u32 = 56;
|
||||||
|
pub const EBADSLT: u32 = 57;
|
||||||
|
pub const EDEADLOCK: u32 = 35;
|
||||||
|
pub const EBFONT: u32 = 59;
|
||||||
|
pub const ENOSTR: u32 = 60;
|
||||||
|
pub const ENODATA: u32 = 61;
|
||||||
|
pub const ETIME: u32 = 62;
|
||||||
|
pub const ENOSR: u32 = 63;
|
||||||
|
pub const ENONET: u32 = 64;
|
||||||
|
pub const ENOPKG: u32 = 65;
|
||||||
|
pub const EREMOTE: u32 = 66;
|
||||||
|
pub const ENOLINK: u32 = 67;
|
||||||
|
pub const EADV: u32 = 68;
|
||||||
|
pub const ESRMNT: u32 = 69;
|
||||||
|
pub const ECOMM: u32 = 70;
|
||||||
|
pub const EPROTO: u32 = 71;
|
||||||
|
pub const EMULTIHOP: u32 = 72;
|
||||||
|
pub const EDOTDOT: u32 = 73;
|
||||||
|
pub const EBADMSG: u32 = 74;
|
||||||
|
pub const EOVERFLOW: u32 = 75;
|
||||||
|
pub const ENOTUNIQ: u32 = 76;
|
||||||
|
pub const EBADFD: u32 = 77;
|
||||||
|
pub const EREMCHG: u32 = 78;
|
||||||
|
pub const ELIBACC: u32 = 79;
|
||||||
|
pub const ELIBBAD: u32 = 80;
|
||||||
|
pub const ELIBSCN: u32 = 81;
|
||||||
|
pub const ELIBMAX: u32 = 82;
|
||||||
|
pub const ELIBEXEC: u32 = 83;
|
||||||
|
pub const EILSEQ: u32 = 84;
|
||||||
|
pub const ERESTART: u32 = 85;
|
||||||
|
pub const ESTRPIPE: u32 = 86;
|
||||||
|
pub const EUSERS: u32 = 87;
|
||||||
|
pub const ENOTSOCK: u32 = 88;
|
||||||
|
pub const EDESTADDRREQ: u32 = 89;
|
||||||
|
pub const EMSGSIZE: u32 = 90;
|
||||||
|
pub const EPROTOTYPE: u32 = 91;
|
||||||
|
pub const ENOPROTOOPT: u32 = 92;
|
||||||
|
pub const EPROTONOSUPPORT: u32 = 93;
|
||||||
|
pub const ESOCKTNOSUPPORT: u32 = 94;
|
||||||
|
pub const EOPNOTSUPP: u32 = 95;
|
||||||
|
pub const EPFNOSUPPORT: u32 = 96;
|
||||||
|
pub const EAFNOSUPPORT: u32 = 97;
|
||||||
|
pub const EADDRINUSE: u32 = 98;
|
||||||
|
pub const EADDRNOTAVAIL: u32 = 99;
|
||||||
|
pub const ENETDOWN: u32 = 100;
|
||||||
|
pub const ENETUNREACH: u32 = 101;
|
||||||
|
pub const ENETRESET: u32 = 102;
|
||||||
|
pub const ECONNABORTED: u32 = 103;
|
||||||
|
pub const ECONNRESET: u32 = 104;
|
||||||
|
pub const ENOBUFS: u32 = 105;
|
||||||
|
pub const EISCONN: u32 = 106;
|
||||||
|
pub const ENOTCONN: u32 = 107;
|
||||||
|
pub const ESHUTDOWN: u32 = 108;
|
||||||
|
pub const ETOOMANYREFS: u32 = 109;
|
||||||
|
pub const ETIMEDOUT: u32 = 110;
|
||||||
|
pub const ECONNREFUSED: u32 = 111;
|
||||||
|
pub const EHOSTDOWN: u32 = 112;
|
||||||
|
pub const EHOSTUNREACH: u32 = 113;
|
||||||
|
pub const EALREADY: u32 = 114;
|
||||||
|
pub const EINPROGRESS: u32 = 115;
|
||||||
|
pub const ESTALE: u32 = 116;
|
||||||
|
pub const EUCLEAN: u32 = 117;
|
||||||
|
pub const ENOTNAM: u32 = 118;
|
||||||
|
pub const ENAVAIL: u32 = 119;
|
||||||
|
pub const EISNAM: u32 = 120;
|
||||||
|
pub const EREMOTEIO: u32 = 121;
|
||||||
|
pub const EDQUOT: u32 = 122;
|
||||||
|
pub const ENOMEDIUM: u32 = 123;
|
||||||
|
pub const EMEDIUMTYPE: u32 = 124;
|
||||||
|
pub const ECANCELED: u32 = 125;
|
||||||
|
pub const ENOKEY: u32 = 126;
|
||||||
|
pub const EKEYEXPIRED: u32 = 127;
|
||||||
|
pub const EKEYREVOKED: u32 = 128;
|
||||||
|
pub const EKEYREJECTED: u32 = 129;
|
||||||
|
pub const EOWNERDEAD: u32 = 130;
|
||||||
|
pub const ENOTRECOVERABLE: u32 = 131;
|
||||||
|
pub const ERFKILL: u32 = 132;
|
||||||
|
pub const EHWPOISON: u32 = 133;
|
||||||
|
pub const ENOTSUP: u32 = 95;
|
||||||
|
pub type wchar_t = ::std::os::raw::c_uint;
|
||||||
|
#[repr(C)]
|
||||||
|
#[repr(align(16))]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct max_align_t {
|
||||||
|
pub __clang_max_align_nonce1: ::std::os::raw::c_longlong,
|
||||||
|
pub __bindgen_padding_0: u64,
|
||||||
|
pub __clang_max_align_nonce2: u128,
|
||||||
|
}
|
||||||
|
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||||
|
const _: () = {
|
||||||
|
["Size of max_align_t"][::std::mem::size_of::<max_align_t>() - 32usize];
|
||||||
|
["Alignment of max_align_t"][::std::mem::align_of::<max_align_t>() - 16usize];
|
||||||
|
["Offset of field: max_align_t::__clang_max_align_nonce1"]
|
||||||
|
[::std::mem::offset_of!(max_align_t, __clang_max_align_nonce1) - 0usize];
|
||||||
|
["Offset of field: max_align_t::__clang_max_align_nonce2"]
|
||||||
|
[::std::mem::offset_of!(max_align_t, __clang_max_align_nonce2) - 16usize];
|
||||||
|
};
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct _GoString_ {
|
||||||
|
pub p: *const ::std::os::raw::c_char,
|
||||||
|
pub n: isize,
|
||||||
|
}
|
||||||
|
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||||
|
const _: () = {
|
||||||
|
["Size of _GoString_"][::std::mem::size_of::<_GoString_>() - 16usize];
|
||||||
|
["Alignment of _GoString_"][::std::mem::align_of::<_GoString_>() - 8usize];
|
||||||
|
["Offset of field: _GoString_::p"][::std::mem::offset_of!(_GoString_, p) - 0usize];
|
||||||
|
["Offset of field: _GoString_::n"][::std::mem::offset_of!(_GoString_, n) - 8usize];
|
||||||
|
};
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn __errno_location() -> *mut ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
pub type GoInt8 = ::std::os::raw::c_schar;
|
||||||
|
pub type GoUint8 = ::std::os::raw::c_uchar;
|
||||||
|
pub type GoInt16 = ::std::os::raw::c_short;
|
||||||
|
pub type GoUint16 = ::std::os::raw::c_ushort;
|
||||||
|
pub type GoInt32 = ::std::os::raw::c_int;
|
||||||
|
pub type GoUint32 = ::std::os::raw::c_uint;
|
||||||
|
pub type GoInt64 = ::std::os::raw::c_longlong;
|
||||||
|
pub type GoUint64 = ::std::os::raw::c_ulonglong;
|
||||||
|
pub type GoInt = GoInt64;
|
||||||
|
pub type GoUint = GoUint64;
|
||||||
|
pub type GoUintptr = usize;
|
||||||
|
pub type GoFloat32 = f32;
|
||||||
|
pub type GoFloat64 = f64;
|
||||||
|
pub type GoComplex64 = __BindgenComplex<f32>;
|
||||||
|
pub type GoComplex128 = __BindgenComplex<f64>;
|
||||||
|
pub type _check_for_64_bit_pointer_matching_GoInt = [::std::os::raw::c_char; 1usize];
|
||||||
|
pub type GoString = _GoString_;
|
||||||
|
pub type GoMap = *mut ::std::os::raw::c_void;
|
||||||
|
pub type GoChan = *mut ::std::os::raw::c_void;
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct GoInterface {
|
||||||
|
pub t: *mut ::std::os::raw::c_void,
|
||||||
|
pub v: *mut ::std::os::raw::c_void,
|
||||||
|
}
|
||||||
|
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||||
|
const _: () = {
|
||||||
|
["Size of GoInterface"][::std::mem::size_of::<GoInterface>() - 16usize];
|
||||||
|
["Alignment of GoInterface"][::std::mem::align_of::<GoInterface>() - 8usize];
|
||||||
|
["Offset of field: GoInterface::t"][::std::mem::offset_of!(GoInterface, t) - 0usize];
|
||||||
|
["Offset of field: GoInterface::v"][::std::mem::offset_of!(GoInterface, v) - 8usize];
|
||||||
|
};
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct GoSlice {
|
||||||
|
pub data: *mut ::std::os::raw::c_void,
|
||||||
|
pub len: GoInt,
|
||||||
|
pub cap: GoInt,
|
||||||
|
}
|
||||||
|
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||||
|
const _: () = {
|
||||||
|
["Size of GoSlice"][::std::mem::size_of::<GoSlice>() - 24usize];
|
||||||
|
["Alignment of GoSlice"][::std::mem::align_of::<GoSlice>() - 8usize];
|
||||||
|
["Offset of field: GoSlice::data"][::std::mem::offset_of!(GoSlice, data) - 0usize];
|
||||||
|
["Offset of field: GoSlice::len"][::std::mem::offset_of!(GoSlice, len) - 8usize];
|
||||||
|
["Offset of field: GoSlice::cap"][::std::mem::offset_of!(GoSlice, cap) - 16usize];
|
||||||
|
};
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetNewServer() -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetStart(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetUp(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetClose(sd: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetGetIps(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
buf: *mut ::std::os::raw::c_char,
|
||||||
|
buflen: usize,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetErrmsg(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
buf: *mut ::std::os::raw::c_char,
|
||||||
|
buflen: usize,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetListen(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
network: *mut ::std::os::raw::c_char,
|
||||||
|
addr: *mut ::std::os::raw::c_char,
|
||||||
|
listenerOut: *mut ::std::os::raw::c_int,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetGetRemoteAddr(
|
||||||
|
listener: ::std::os::raw::c_int,
|
||||||
|
conn: ::std::os::raw::c_int,
|
||||||
|
buf: *mut ::std::os::raw::c_char,
|
||||||
|
buflen: usize,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetDial(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
network: *mut ::std::os::raw::c_char,
|
||||||
|
addr: *mut ::std::os::raw::c_char,
|
||||||
|
connOut: *mut ::std::os::raw::c_int,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetSetDir(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
str_: *mut ::std::os::raw::c_char,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetSetHostname(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
str_: *mut ::std::os::raw::c_char,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetSetAuthKey(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
str_: *mut ::std::os::raw::c_char,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetSetControlURL(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
str_: *mut ::std::os::raw::c_char,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetSetEphemeral(sd: ::std::os::raw::c_int, e: GoInt) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetSetLogFD(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
fd: ::std::os::raw::c_int,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetLoopback(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
addrOut: *mut ::std::os::raw::c_char,
|
||||||
|
addrLen: usize,
|
||||||
|
proxyOut: *mut ::std::os::raw::c_char,
|
||||||
|
localOut: *mut ::std::os::raw::c_char,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
|
unsafe extern "C" {
|
||||||
|
pub fn TsnetEnableFunnelToLocalhostPlaintextHttp1(
|
||||||
|
sd: ::std::os::raw::c_int,
|
||||||
|
localhostPort: ::std::os::raw::c_int,
|
||||||
|
) -> ::std::os::raw::c_int;
|
||||||
|
}
|
||||||
350
src-tauri/tailscale/src/lib.rs
Normal file
350
src-tauri/tailscale/src/lib.rs
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use std::os::fd::{AsRawFd, RawFd};
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::io::{AsRawHandle, RawHandle};
|
||||||
|
use std::os::raw::{c_char, c_int};
|
||||||
|
mod bindings;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
type GoInt = i64;
|
||||||
|
|
||||||
|
use bindings::*;
|
||||||
|
use libc;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TailscaleError {
|
||||||
|
ApiError(c_int, String),
|
||||||
|
BadFileDescriptor,
|
||||||
|
BufferTooSmall,
|
||||||
|
NulError(std::ffi::NulError),
|
||||||
|
InvalidUtf8(std::str::Utf8Error),
|
||||||
|
IoError(std::io::Error),
|
||||||
|
InvalidHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::ffi::NulError> for TailscaleError {
|
||||||
|
fn from(err: std::ffi::NulError) -> Self {
|
||||||
|
TailscaleError::NulError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::str::Utf8Error> for TailscaleError {
|
||||||
|
fn from(err: std::str::Utf8Error) -> Self {
|
||||||
|
TailscaleError::InvalidUtf8(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for TailscaleError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
TailscaleError::IoError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get error message from the server handle
|
||||||
|
// This helper is needed because TsnetErrmsg requires the server handle (sd)
|
||||||
|
fn get_tsnet_errmsg(sd: c_int) -> String {
|
||||||
|
let mut buf = [0u8; 256]; // Choose a reasonable buffer size
|
||||||
|
let message = unsafe { TsnetErrmsg(sd, buf.as_mut_ptr() as *mut c_char, buf.len()) };
|
||||||
|
|
||||||
|
if message == 0 {
|
||||||
|
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
|
||||||
|
c_str.to_string_lossy().into_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"(Failed to get error message, TsnetErrmsg returned {})",
|
||||||
|
message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tsnet_result(sd: c_int, ret: c_int) -> Result<(), TailscaleError> {
|
||||||
|
match ret {
|
||||||
|
0 => Ok(()),
|
||||||
|
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
|
||||||
|
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
|
||||||
|
_ => {
|
||||||
|
let message = get_tsnet_errmsg(sd);
|
||||||
|
Err(TailscaleError::ApiError(ret, message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Tailscale(c_int);
|
||||||
|
|
||||||
|
// A TailscaleListener is a socket on the tailnet listening for connections.
|
||||||
|
//
|
||||||
|
// It is much like allocating a system socket(2) and calling listen(2).
|
||||||
|
// Accept connections with tailscale_accept and close the listener with close.
|
||||||
|
//
|
||||||
|
// Under the hood, a tailscale_listener is one half of a socketpair itself,
|
||||||
|
// used to move the connection fd from Go to C. This means you can use epoll
|
||||||
|
// or its equivalent on a tailscale_listener to know if there is a connection
|
||||||
|
// read to accept.
|
||||||
|
pub struct TailscaleListener(c_int);
|
||||||
|
|
||||||
|
// A TailscaleConn is a connection to an address on the tailnet.
|
||||||
|
//
|
||||||
|
// It is a pipe(2) on which you can use read(2), write(2), and close(2).
|
||||||
|
// For extra control over the connection, see the tailscale_conn_* functions.
|
||||||
|
pub struct TailscaleConn(c_int);
|
||||||
|
|
||||||
|
// NEEDS REVIEW. CANNOT BE BADLY DONE
|
||||||
|
impl Drop for Tailscale {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let ret = unsafe { TsnetClose(self.0) };
|
||||||
|
if ret != 0 && ret != libc::EBADF {
|
||||||
|
eprintln!("Error closing Tailscale server {}: {}", self.0, ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TailscaleListener {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// TailscaleListener is treated like a file descriptor.
|
||||||
|
let ret = unsafe { libc::close(self.0) };
|
||||||
|
if ret != 0 && ret != libc::EBADF {
|
||||||
|
eprintln!("Error closing Tailscale listener {}: {}", self.0, ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TailscaleConn {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// TailscaleConn is treated like a file descriptor (pipe).
|
||||||
|
let ret = unsafe { libc::close(self.0) };
|
||||||
|
if ret != 0 && ret != libc::EBADF {
|
||||||
|
eprintln!("Error closing Tailscale connection {}: {}", self.0, ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tailscale {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Tailscale(unsafe { TsnetNewServer() })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> Result<(), TailscaleError> {
|
||||||
|
let ret = unsafe { TsnetStart(self.0) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn up(&self) -> Result<(), TailscaleError> {
|
||||||
|
let ret = unsafe { TsnetUp(self.0) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&self) -> Result<(), TailscaleError> {
|
||||||
|
let ret = unsafe { TsnetClose(self.0) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dir(&self, dir: &str) -> Result<(), TailscaleError> {
|
||||||
|
let c_dir = CString::new(dir)?;
|
||||||
|
let ret = unsafe { TsnetSetDir(self.0, c_dir.as_ptr() as *mut c_char) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_hostname<T: AsRef<str>>(&self, hostname: T) -> Result<(), TailscaleError> {
|
||||||
|
let c_hostname = CString::new(hostname.as_ref())?;
|
||||||
|
let ret = unsafe { TsnetSetHostname(self.0, c_hostname.as_ptr() as *mut c_char) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_authkey(&self, authkey: &str) -> Result<(), TailscaleError> {
|
||||||
|
let c_authkey = CString::new(authkey)?;
|
||||||
|
let ret = unsafe { TsnetSetAuthKey(self.0, c_authkey.as_ptr() as *mut c_char) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_control_url(&self, control_url: &str) -> Result<(), TailscaleError> {
|
||||||
|
let c_control_url = CString::new(control_url)?;
|
||||||
|
let ret = unsafe { TsnetSetControlURL(self.0, c_control_url.as_ptr() as *mut c_char) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ephemeral(&self, ephemeral: bool) -> Result<(), TailscaleError> {
|
||||||
|
let e: GoInt = if ephemeral { 1 } else { 0 };
|
||||||
|
// Use GoInt (i64) based on bindgen output
|
||||||
|
let ret = unsafe { TsnetSetEphemeral(self.0, e) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_log_fd(&self, fd: i32) -> Result<(), TailscaleError> {
|
||||||
|
let ret = unsafe { TsnetSetLogFD(self.0, fd) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ips<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str, TailscaleError> {
|
||||||
|
let ret = unsafe { TsnetGetIps(self.0, buf.as_mut_ptr() as *mut c_char, buf.len()) };
|
||||||
|
match ret {
|
||||||
|
0 => {
|
||||||
|
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
|
||||||
|
c_str.to_str().map_err(TailscaleError::from) // Convert Utf8Error
|
||||||
|
}
|
||||||
|
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
|
||||||
|
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
|
||||||
|
_ => {
|
||||||
|
let err_msg = get_tsnet_errmsg(self.0);
|
||||||
|
Err(TailscaleError::ApiError(ret, err_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loopback(
|
||||||
|
&self,
|
||||||
|
addr_buf: &mut [u8],
|
||||||
|
proxy_buf: &mut [u8],
|
||||||
|
local_buf: &mut [u8],
|
||||||
|
) -> Result<(), TailscaleError> {
|
||||||
|
// C header says proxy_out and local_out must hold 33 bytes.
|
||||||
|
if proxy_buf.len() < 33 || local_buf.len() < 33 {
|
||||||
|
return Err(TailscaleError::BufferTooSmall); // Custom check based on docs
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = unsafe {
|
||||||
|
TsnetLoopback(
|
||||||
|
self.0,
|
||||||
|
addr_buf.as_mut_ptr() as *mut c_char,
|
||||||
|
addr_buf.len(),
|
||||||
|
proxy_buf.as_mut_ptr() as *mut c_char,
|
||||||
|
local_buf.as_mut_ptr() as *mut c_char,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
pub fn dial(&self, network: &str, addr: &str) -> Result<TailscaleConn, TailscaleError> {
|
||||||
|
let c_network = CString::new(network)?;
|
||||||
|
let c_addr = CString::new(addr)?;
|
||||||
|
let mut conn_out: c_int = -1;
|
||||||
|
let ret = unsafe {
|
||||||
|
TsnetDial(
|
||||||
|
self.0,
|
||||||
|
c_network.as_ptr() as *mut c_char,
|
||||||
|
c_addr.as_ptr() as *mut c_char,
|
||||||
|
&mut conn_out,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_tsnet_result(self.0, ret)?;
|
||||||
|
|
||||||
|
if ret == 0 && conn_out != -1 {
|
||||||
|
Ok(TailscaleConn(conn_out))
|
||||||
|
} else if ret == 0 {
|
||||||
|
Err(TailscaleError::InvalidHandle)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn listen(&self, network: &str, addr: &str) -> Result<TailscaleListener, TailscaleError> {
|
||||||
|
let c_network = CString::new(network)?;
|
||||||
|
let c_addr = CString::new(addr)?;
|
||||||
|
let mut listener_out: c_int = -1; // Use c_int for the output pointer
|
||||||
|
let ret = unsafe {
|
||||||
|
TsnetListen(
|
||||||
|
self.0,
|
||||||
|
c_network.as_ptr() as *mut c_char,
|
||||||
|
c_addr.as_ptr() as *mut c_char,
|
||||||
|
&mut listener_out,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_tsnet_result(self.0, ret)?;
|
||||||
|
|
||||||
|
if ret == 0 && listener_out != -1 {
|
||||||
|
Ok(TailscaleListener(listener_out))
|
||||||
|
} else if ret == 0 {
|
||||||
|
Err(TailscaleError::InvalidHandle)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_funnel_to_localhost_plaintext_http1(
|
||||||
|
&self,
|
||||||
|
localhost_port: i32,
|
||||||
|
) -> Result<(), TailscaleError> {
|
||||||
|
let ret =
|
||||||
|
unsafe { TsnetEnableFunnelToLocalhostPlaintextHttp1(self.0, localhost_port as c_int) };
|
||||||
|
parse_tsnet_result(self.0, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_last_error_message<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str, TailscaleError> {
|
||||||
|
let ret = unsafe { TsnetErrmsg(self.0, buf.as_mut_ptr() as *mut c_char, buf.len()) };
|
||||||
|
match ret {
|
||||||
|
0 => {
|
||||||
|
let c_str = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
|
||||||
|
c_str.to_str().map_err(TailscaleError::from) // Convert Utf8Error
|
||||||
|
}
|
||||||
|
code if code == libc::EBADF => Err(TailscaleError::BadFileDescriptor),
|
||||||
|
code if code == libc::ERANGE => Err(TailscaleError::BufferTooSmall),
|
||||||
|
// TsnetErrmsg should ideally not return other codes, but handle defensively
|
||||||
|
_ => Err(TailscaleError::ApiError(
|
||||||
|
ret,
|
||||||
|
format!("TsnetErrmsg returned unknown code {}", ret),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
// Requires the connection handle to behave like a raw file descriptor.
|
||||||
|
impl AsRawFd for TailscaleConn {
|
||||||
|
fn as_raw_fd(&self) -> RawFd {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
impl AsRawHandle for TailscaleConn {
|
||||||
|
fn as_raw_handle(&self) -> RawHandle {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for TailscaleConn {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let fd = self.as_raw_fd();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let fd = self.as_raw_handle();
|
||||||
|
// Safety: Calling libc::read on a valid file descriptor.
|
||||||
|
// The caller must ensure the handle is valid for reading (it is after successful dial/accept).
|
||||||
|
let n = unsafe {
|
||||||
|
libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
|
||||||
|
};
|
||||||
|
if n < 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(n as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for TailscaleConn {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
|
||||||
|
let fd = self.as_raw_fd();
|
||||||
|
// Safety: Calling libc::write on a valid file descriptor.
|
||||||
|
// The caller must ensure the handle is valid for writing (it is after successful dial/accept).
|
||||||
|
let n = unsafe {
|
||||||
|
libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len())
|
||||||
|
};
|
||||||
|
if n < 0 {
|
||||||
|
Err(std::io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(n as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
// For a pipe/socket, flush is often a no-op after write.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src-tauri/tailscale/src/test.rs
Normal file
30
src-tauri/tailscale/src/test.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::{Tailscale, TailscaleError};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_listener() -> Result<(), TailscaleError> {
|
||||||
|
println!("Creating server");
|
||||||
|
// Create a new server
|
||||||
|
let ts = Tailscale::new();
|
||||||
|
|
||||||
|
// Configure it
|
||||||
|
println!("Configuring directory");
|
||||||
|
ts.set_dir("/tmp/tailscale-rust-test")?;
|
||||||
|
println!("Configuring hostname");
|
||||||
|
ts.set_hostname("my-rust-node")?;
|
||||||
|
println!("Setting ephemeral");
|
||||||
|
//ts.set_authkey("tskey-...")?; // Set authkey if needed for auto-registration
|
||||||
|
ts.set_ephemeral(true)?;
|
||||||
|
|
||||||
|
// Bring the server up
|
||||||
|
println!("Starting Tailscale...");
|
||||||
|
ts.up()?;
|
||||||
|
println!("Tailscale started!");
|
||||||
|
|
||||||
|
// Get IPs
|
||||||
|
let mut ip_buf = [0u8; 256];
|
||||||
|
let ips = ts.get_ips(&mut ip_buf)?;
|
||||||
|
println!("Tailscale IPs: {}", ips);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||||
"productName": "Drop Desktop Client",
|
"productName": "Drop Desktop Client",
|
||||||
"version": "0.2.0-beta",
|
"version": "0.3.0-rc-2",
|
||||||
"identifier": "dev.drop.app",
|
"identifier": "dev.drop.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn dev --port 1432",
|
"beforeDevCommand": "yarn dev --port 1432",
|
||||||
|
|||||||
12
types.ts
12
types.ts
@ -17,7 +17,7 @@ export type User = {
|
|||||||
username: string;
|
username: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profilePicture: string;
|
profilePictureObjectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppState = {
|
export type AppState = {
|
||||||
@ -30,11 +30,11 @@ export type Game = {
|
|||||||
mName: string;
|
mName: string;
|
||||||
mShortDescription: string;
|
mShortDescription: string;
|
||||||
mDescription: string;
|
mDescription: string;
|
||||||
mIconId: string;
|
mIconObjectId: string;
|
||||||
mBannerId: string;
|
mBannerObjectId: string;
|
||||||
mCoverId: string;
|
mCoverObjectId: string;
|
||||||
mImageLibrary: string[];
|
mImageLibraryObjectIds: string[];
|
||||||
mImageCarousel: string[];
|
mImageCarouselObjectIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GameVersion = {
|
export type GameVersion = {
|
||||||
|
|||||||
Reference in New Issue
Block a user