Compare commits

..

3 Commits

Author SHA1 Message Date
43b56462d6 feat: more refactoring (broken) 2025-09-16 15:09:43 +10:00
ab219670dc fix: cleanup dependencies 2025-09-14 09:27:49 +10:00
c1beef380e refactor: into rust workspaces 2025-09-14 09:19:03 +10:00
151 changed files with 1957 additions and 17522 deletions

View File

@ -1,21 +1,29 @@
# Drop Desktop Client
# Drop App
The Drop Desktop Client is the companion app for [Drop](https://github.com/Drop-OSS/drop). It is the official & intended way to download and play games on your Drop server.
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
## Internals
## Running
Before setting up the drop app, be sure that you have a server set up.
The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
## Current features
Currently supported are the following features:
- Signin (with custom server)
- Database registering & recovery
- Dynamic library fetching from server
- Installing & uninstalling games
- Download progress monitoring
- Launching / playing games
## Development
Before setting up a development environemnt, be sure that you have a server set up. The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart).
Then, install dependencies with `yarn`. This'll install the custom builder's dependencies. Then, check everything works properly with `yarn tauri build`.
Install dependencies with `yarn`
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh`
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
e.g. `RUST_LOG=debug yarn tauri dev`
## Contributing
Check out the contributing guide on our Developer Docs: [Drop Developer Docs - Contributing](https://developer.droposs.org/contributing).
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.

View File

@ -1,5 +1,5 @@
<template>
<NuxtLoadingIndicator color="#2563eb" />
<NuxtLoadingIndicator color="#2563eb" />
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
@ -7,7 +7,14 @@
</template>
<script setup lang="ts">
import "~/composables/downloads.js";
import { invoke } from "@tauri-apps/api/core";
import { useAppState } from "./composables/app-state.js";
import {
initialNavigation,
setupHooks,
} from "./composables/state-navigation.js";
const router = useRouter();

View File

@ -2,7 +2,9 @@
<div class="h-16 bg-zinc-950 flex flex-row justify-between">
<div class="flex flex-row grow items-center pl-5 pr-2 py-3">
<div class="inline-flex items-center gap-x-10">
<Wordmark class="h-8 mb-0.5" />
<NuxtLink to="/store">
<Wordmark class="h-8 mb-0.5" />
</NuxtLink>
<nav class="inline-flex items-center mt-0.5">
<ol class="inline-flex items-center gap-x-6">
<NuxtLink
@ -40,7 +42,7 @@
</ol>
</div>
</div>
<WindowControl />
<WindowControl />
</div>
</template>

View File

@ -76,6 +76,7 @@ import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
import type { NavigationItem } from "../types";
import HeaderWidget from "./HeaderWidget.vue";
import { useAppState } from "~/composables/app-state";
import { invoke } from "@tauri-apps/api/core";
const open = ref(false);

View File

@ -73,7 +73,7 @@
alt=""
/>
</div>
<div class="flex flex-col gap-x-2">
<div class="inline-flex items-center gap-x-2">
<p
class="text-sm whitespace-nowrap font-display font-semibold"
>

View File

@ -0,0 +1,7 @@
<template>
<NuxtLink
class="inline-flex items-center gap-x-2 px-1 py-0.5 rounded bg-blue-900 text-zinc-100 hover:bg-blue-800"
>
<slot />
</NuxtLink>
</template>

View File

@ -1,5 +1,5 @@
import { convertFileSrc } from "@tauri-apps/api/core";
export const useObject = (id: string) => {
export const useObject = async (id: string) => {
return convertFileSrc(id, "object");
};

View File

@ -9,17 +9,13 @@ export default defineNuxtConfig({
},
},
css: ["~/assets/main.scss"],
ssr: false,
extends: ["../shared", "../libs/drop-base"],
extends: [["../libs/drop-base"]],
app: {
baseURL: "/main",
},
devtools: {
enabled: false,
},
}
});

View File

@ -7,7 +7,6 @@ export default {
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"../shared/components/**/*.vue"
],
theme: {
extend: {

View File

@ -1,50 +0,0 @@
<template>
<NuxtLoadingIndicator color="#2563eb" />
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
</NuxtLayout>
</template>
<script setup lang="ts">
import "~/composables/downloads.js";
import { invoke } from "@tauri-apps/api/core";
import { useAppState } from "./composables/app-state.js";
import {
initialNavigation,
setupHooks,
} from "./composables/state-navigation.js";
const router = useRouter();
const state = useAppState();
async function fetchState() {
try {
state.value = JSON.parse(await invoke("fetch_state"));
if (!state.value)
throw createError({
statusCode: 500,
statusMessage: `App state is: ${state.value}`,
fatal: true,
});
} catch (e) {
console.error("failed to parse state", e);
throw e;
}
}
await fetchState();
// This is inefficient but apparently we do it lol
router.beforeEach(async () => {
await fetchState();
});
setupHooks();
initialNavigation(state);
useHead({
title: "Drop",
});
</script>

View File

@ -1,84 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
-ms-overflow-style: none; /* IE and Edge /
scrollbar-width: none; / Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
html::-webkit-scrollbar {
display: none;
}
$motiva: (
("MotivaSansThin.ttf", "ttf", 100, normal),
("MotivaSansLight.woff.ttf", "woff", 300, normal),
("MotivaSansRegular.woff.ttf", "woff", 400, normal),
("MotivaSansMedium.woff.ttf", "woff", 500, normal),
("MotivaSansBold.woff.ttf", "woff", 600, normal),
("MotivaSansExtraBold.ttf", "woff", 700, normal),
("MotivaSansBlack.woff.ttf", "woff", 900, normal)
);
$helvetica: (
("Helvetica.woff", "woff", 400, normal),
("Helvetica-Oblique.woff", "woff", 400, italic),
("Helvetica-Bold.woff", "woff", 600, normal),
("Helvetica-BoldOblique.woff", "woff", 600, italic),
("helvetica-light-587ebe5a59211.woff2", "woff2", 300, normal)
);
@each $file, $format, $weight, $style in $motiva {
@font-face {
font-family: "Motiva Sans";
src: url("/fonts/motiva/#{$file}") format($format);
font-weight: $weight;
font-style: $style;
}
}
@each $file, $format, $weight, $style in $helvetica {
@font-face {
font-family: "Helvetica";
src: url("/fonts/helvetica/#{$file}") format($format);
font-weight: $weight;
font-style: $style;
}
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter/InterVariable.ttf");
font-style: normal;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter/InterVariable-Italic.ttf");
font-style: italic;
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
scrollbar-width: 4px;
scrollbar-color: #52525b #00000000;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 4px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: #52525b;
border-radius: 10px;
border: 3px solid #52525b;
}

View File

@ -1,91 +0,0 @@
<template>
<NuxtLayout name="default">
<div
class="grid min-h-full grid-cols-1 grid-rows-[1fr,auto,1fr] lg:grid-cols-[max(50%,36rem),1fr]"
>
<header
class="mx-auto w-full max-w-7xl px-6 pt-6 sm:pt-10 lg:col-span-2 lg:col-start-1 lg:row-start-1 lg:px-8"
>
<Logo class="h-10 w-auto sm:h-12" />
</header>
<main
class="mx-auto w-full max-w-7xl px-6 py-24 sm:py-32 lg:col-span-2 lg:col-start-1 lg:row-start-2 lg:px-8"
>
<div class="max-w-lg">
<p class="text-base font-semibold leading-8 text-blue-600">
{{ error?.statusCode }}
</p>
<h1
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
>
Oh no!
</h1>
<p
v-if="message"
class="mt-3 font-bold text-base leading-7 text-red-500"
>
{{ message }}
</p>
<p class="mt-6 text-base leading-7 text-zinc-400">
An error occurred while responding to your request. If you believe
this to be a bug, please report it. Try signing in and see if it
resolves the issue.
</p>
<div class="mt-10">
<!-- full app reload to fix errors -->
<a
href="/store"
class="text-sm font-semibold leading-7 text-blue-600"
><span aria-hidden="true">&larr;</span> Back to store</a
>
</div>
</div>
</main>
<footer class="self-end lg:col-span-2 lg:col-start-1 lg:row-start-3">
<div class="border-t border-zinc-700 bg-zinc-900 py-10">
<nav
class="mx-auto flex w-full max-w-7xl items-center gap-x-4 px-6 text-sm leading-7 text-zinc-400 lg:px-8"
>
<NuxtLink href="/docs">Documentation</NuxtLink>
<svg
viewBox="0 0 2 2"
aria-hidden="true"
class="h-0.5 w-0.5 fill-zinc-600"
>
<circle cx="1" cy="1" r="1" />
</svg>
<a href="https://discord.gg/NHx46XKJWA" target="_blank"
>Support Discord</a
>
</nav>
</div>
</footer>
<div
class="hidden lg:relative lg:col-start-2 lg:row-start-1 lg:row-end-4 lg:block"
>
<img
src="@/assets/wallpaper.jpg"
alt=""
class="absolute inset-0 h-full w-full object-cover"
/>
</div>
</div>
</NuxtLayout>
</template>
<script setup lang="ts">
import type { NuxtError } from "#app";
const props = defineProps({
error: Object as () => NuxtError,
});
const statusCode = props.error?.statusCode;
const message =
props.error?.statusMessage ||
props.error?.message ||
"An unknown error occurred.";
console.error(props.error);
</script>

View File

@ -1,25 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-04-03",
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
css: ["~/assets/main.scss"],
ssr: false,
extends: [["../libs/drop-base"]],
app: {
baseURL: "/main",
},
devtools: {
enabled: false,
},
});

View File

@ -1,37 +0,0 @@
{
"name": "view",
"private": true,
"version": "0.3.3",
"type": "module",
"scripts": {
"build": "nuxt generate",
"dev": "nuxt dev",
"postinstall": "nuxt prepare",
"tauri": "tauri"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tauri-apps/api": "^2.7.0",
"koa": "^2.16.1",
"markdown-it": "^14.1.0",
"micromark": "^4.0.1",
"nuxt": "^3.16.0",
"scss": "^0.2.4",
"vue-router": "latest",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/markdown-it": "^14.1.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -1,20 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
],
theme: {
extend: {
fontFamily: {
sans: ["Inter"],
display: ["Motiva Sans"],
},
},
},
plugins: [require("@tailwindcss/forms"), require('@tailwindcss/typography')],
};

View File

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

View File

@ -1,96 +0,0 @@
import type { Component } from "vue";
export type NavigationItem = {
prefix: string;
route: string;
label: string;
};
export type QuickActionNav = {
icon: Component;
notifications?: number;
action: () => Promise<void>;
};
export type User = {
id: string;
username: string;
admin: boolean;
displayName: string;
profilePictureObjectId: string;
};
export type AppState = {
status: AppStatus;
user?: User;
};
export type Game = {
id: string;
mName: string;
mShortDescription: string;
mDescription: string;
mIconObjectId: string;
mBannerObjectId: string;
mCoverObjectId: string;
mImageLibraryObjectIds: string[];
mImageCarouselObjectIds: string[];
};
export type Collection = {
id: string;
name: string;
isDefault: boolean;
entries: Array<{ gameId: string; game: Game }>;
};
export type GameVersion = {
launchCommandTemplate: string;
};
export enum AppStatus {
NotConfigured = "NotConfigured",
Offline = "Offline",
SignedOut = "SignedOut",
SignedIn = "SignedIn",
SignedInNeedsReauth = "SignedInNeedsReauth",
ServerUnavailable = "ServerUnavailable",
}
export enum GameStatusEnum {
Remote = "Remote",
Queued = "Queued",
Downloading = "Downloading",
Validating = "Validating",
Installed = "Installed",
Updating = "Updating",
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
Running = "Running",
PartiallyInstalled = "PartiallyInstalled",
}
export type GameStatus = {
type: GameStatusEnum;
version_name?: string;
install_dir?: string;
};
export enum DownloadableType {
Game = "Game",
Tool = "Tool",
DLC = "DLC",
Mod = "Mod",
}
export type DownloadableMetadata = {
id: string;
version: string;
downloadType: DownloadableType;
};
export type Settings = {
autostart: boolean;
maxDownloadThreads: number;
forceOffline: boolean;
};

File diff suppressed because it is too large Load Diff

1450
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +1,101 @@
[package]
name = "drop-app"
version = "0.3.3"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
# authors = ["Drop OSS"]
edition = "2024"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
[workspace]
resolver = "3"
members = ["drop-consts",
"drop-database",
"drop-downloads",
"drop-errors", "drop-library",
"drop-native-library",
"drop-process",
"drop-remote",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
[lib]
crate-type = ["cdylib", "rlib", "staticlib"]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "drop_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
rustflags = ["-C", "target-feature=+aes,+sse2"]
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
# rustflags = ["-C", "target-feature=+aes,+sse2"]
[dependencies]
tauri-plugin-shell = "2.2.1"
serde_json = "1"
rayon = "1.10.0"
webbrowser = "1.0.2"
url = "2.5.2"
tauri-plugin-deep-link = "2"
log = "0.4.22"
hex = "0.4.3"
tauri-plugin-dialog = "2"
http = "1.1.0"
urlencoding = "2.1.3"
md5 = "0.7.0"
chrono = "0.4.38"
tauri-plugin-os = "2"
boxcar = "0.2.7"
umu-wrapper-lib = "0.1.0"
tauri-plugin-autostart = "2.0.0"
shared_child = "1.0.1"
serde_with = "3.12.0"
slice-deque = "0.3.0"
throttle_my_fn = "0.2.6"
parking_lot = "0.12.3"
atomic-instant-full = "0.1.0"
cacache = "13.1.0"
http-serde = "2.1.1"
reqwest-middleware = "0.4.0"
reqwest-middleware-cache = "0.1.1"
deranged = "=0.4.0"
droplet-rs = "0.7.3"
gethostname = "1.0.1"
zstd = "0.13.3"
tar = "0.4.44"
rand = "0.9.1"
regex = "1.11.1"
tempfile = "3.19.1"
schemars = "0.8.22"
sha1 = "0.10.6"
dirs = "6.0.0"
whoami = "1.6.0"
filetime = "0.2.25"
walkdir = "2.5.0"
known-folders = "1.2.0"
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"
drop-database = { path = "./drop-database" }
drop-downloads = { path = "./drop-downloads" }
drop-errors = { path = "./drop-errors" }
drop-native-library = { path = "./drop-native-library" }
drop-process = { path = "./drop-process" }
drop-remote = { path = "./drop-remote" }
futures-lite = "2.6.0"
page_size = "0.6.0"
sysinfo = "0.36.1"
humansize = "2.1.3"
tokio-util = { version = "0.7.16", features = ["io"] }
futures-core = "0.3.31"
bytes = "1.10.1"
# tailscale = { path = "./tailscale" }
[dependencies.dynfmt]
version = "0.1.5"
features = ["curly"]
[dependencies.tauri]
version = "2.7.0"
features = ["protocol-asset", "tray-icon"]
[dependencies.tokio]
version = "1.40.0"
features = ["rt", "tokio-macros", "signal"]
hex = "0.4.3"
http = "1.1.0"
known-folders = "1.2.0"
log = "0.4.22"
md5 = "0.7.0"
rayon = "1.10.0"
regex = "1.11.1"
reqwest-websocket = "0.5.0"
serde_json = "1"
tar = "0.4.44"
tauri = { version = "2.7.0", features = ["protocol-asset", "tray-icon"] }
tauri-plugin-autostart = "2.0.0"
tauri-plugin-deep-link = "2"
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2.4.0"
tauri-plugin-os = "2"
tauri-plugin-shell = "2.2.1"
tempfile = "3.19.1"
url = "2.5.2"
webbrowser = "1.0.2"
whoami = "1.6.0"
zstd = "0.13.3"
[dependencies.log4rs]
version = "1.3.0"
features = ["console_appender", "file_appender"]
[dependencies.rustix]
version = "0.38.37"
features = ["fs"]
[dependencies.uuid]
version = "1.10.0"
features = ["v4", "fast-rng", "macro-diagnostics"]
[dependencies.rustbreak]
version = "2"
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
[dependencies.reqwest]
version = "0.12.22"
default-features = false
features = [
"json",
"http2",
"blocking",
"rustls-tls",
"native-tls-alpn",
"rustls-tls-native-roots",
"stream",
"blocking",
"http2",
"json",
"native-tls-alpn",
"rustls-tls",
"rustls-tls-native-roots",
"stream",
]
[dependencies.rustix]
version = "0.38.37"
features = ["fs"]
[dependencies.serde]
version = "1"
features = ["derive", "rc"]
[dependencies.uuid]
version = "1.10.0"
features = ["fast-rng", "macro-diagnostics", "v4"]
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
[profile.release]
lto = true
panic = "abort"
codegen-units = 1
panic = 'abort'

View File

@ -0,0 +1,7 @@
[package]
name = "drop-consts"
version = "0.1.0"
edition = "2024"
[dependencies]
dirs = "6.0.0"

View File

@ -0,0 +1,15 @@
use std::{
path::PathBuf,
sync::{Arc, LazyLock},
};
#[cfg(not(debug_assertions))]
static DATA_ROOT_PREFIX: &'static str = "drop";
#[cfg(debug_assertions)]
static DATA_ROOT_PREFIX: &str = "drop-debug";
pub static DATA_ROOT_DIR: LazyLock<&'static PathBuf> =
LazyLock::new(|| Box::leak(Box::new(dirs::data_dir().unwrap().join(DATA_ROOT_PREFIX))));
pub static CACHE_DIR: LazyLock<&'static PathBuf> =
LazyLock::new(|| Box::leak(Box::new(DATA_ROOT_DIR.join("cache"))));

View File

@ -0,0 +1,21 @@
[package]
name = "drop-database"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "0.6.7"
chrono = "0.4.42"
drop-consts = { path = "../drop-consts" }
drop-library = { path = "../drop-library" }
drop-native-library = { path = "../drop-native-library" }
log = "0.4.28"
native_model = { git = "https://github.com/Drop-OSS/native_model.git", version = "0.6.4", features = [
"rmp_serde_1_3",
] }
rustbreak = "2.0.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_with = "3.14.0"
url = "2.5.7"
whoami = "1.6.1"

View File

@ -7,23 +7,15 @@ use std::{
};
use chrono::Utc;
use drop_consts::DATA_ROOT_DIR;
use log::{debug, error, info, warn};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{Serialize, de::DeserializeOwned};
use url::Url;
use crate::DB;
use super::models::data::Database;
#[cfg(not(debug_assertions))]
static DATA_ROOT_PREFIX: &'static str = "drop";
#[cfg(debug_assertions)]
static DATA_ROOT_PREFIX: &str = "drop-debug";
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> =
LazyLock::new(|| Arc::new(dirs::data_dir().unwrap().join(DATA_ROOT_PREFIX)));
// Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)]
pub struct DropDatabaseSerializer;
@ -32,16 +24,15 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::encode(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, mut s: R) -> rustbreak::error::DeSerResult<T> {
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(buf)
.map_err(|e| DeSerError::Internal(e.to_string()))?;
.map_err(|e| rustbreak::error::DeSerError::Internal(e.to_string()))?;
let (val, _version) =
native_model::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
}
}
@ -51,8 +42,6 @@ pub type DatabaseInterface =
pub trait DatabaseImpls {
fn set_up_database() -> DatabaseInterface;
fn database_is_set_up(&self) -> bool;
fn fetch_base_url(&self) -> Url;
}
impl DatabaseImpls for DatabaseInterface {
fn set_up_database() -> DatabaseInterface {
@ -77,7 +66,7 @@ impl DatabaseImpls for DatabaseInterface {
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
}
} else {
let default = Database::new(games_base_dir, None, cache_dir);
let default = Database::new(games_base_dir, None);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
@ -85,15 +74,6 @@ impl DatabaseImpls for DatabaseInterface {
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
}
}
fn database_is_set_up(&self) -> bool {
!self.borrow_data().unwrap().base_url.is_empty()
}
fn fetch_base_url(&self) -> Url {
let handle = self.borrow_data().unwrap();
Url::parse(&handle.base_url).unwrap()
}
}
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
@ -116,15 +96,14 @@ fn handle_invalid_database(
let db = Database::new(
games_base_dir.into_os_string().into_string().unwrap(),
Some(new_path),
cache_dir,
);
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
}
// To automatically save the database upon drop
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
pub struct DBRead<'a>(pub(crate) RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(pub(crate) ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = Database;
@ -159,23 +138,3 @@ impl Drop for DBWrite<'_> {
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => DBRead(data),
Err(e) => {
error!("database borrow failed with error {e}");
panic!("database borrow failed with error {e}");
}
}
}
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() {
Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => {
error!("database borrow mut failed with error {e}");
panic!("database borrow mut failed with error {e}");
}
}
}

View File

@ -9,7 +9,7 @@ pub type DropData = v1::DropData;
pub static DROP_DATA_PATH: &str = ".dropdata";
pub mod v1 {
mod v1 {
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
use native_model::native_model;

View File

@ -0,0 +1,34 @@
use std::{mem::ManuallyDrop, sync::LazyLock};
use log::error;
use crate::db::{DBRead, DBWrite, DatabaseImpls, DatabaseInterface};
pub mod db;
pub mod debug;
pub mod models;
pub mod process;
pub mod runtime_models;
pub mod drop_data;
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() {
Ok(data) => DBRead(data),
Err(e) => {
error!("database borrow failed with error {e}");
panic!("database borrow failed with error {e}");
}
}
}
pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() {
Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => {
error!("database borrow mut failed with error {e}");
panic!("database borrow mut failed with error {e}");
}
}
}

View File

@ -4,11 +4,11 @@ pub mod data {
use native_model::native_model;
use serde::{Deserialize, Serialize};
// NOTE: Within each version, you should NEVER use these types.
// NOTE: Within each version, you should NEVER use these types.
// Declare it using the actual version that it is from, i.e. v1::Settings rather than just Settings from here
pub type GameVersion = v1::GameVersion;
pub type Database = v3::Database;
pub type Database = v4::Database;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
@ -19,7 +19,7 @@ pub mod data {
*/
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseApplications = v4::DatabaseApplications;
// pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::collections::HashMap;
@ -37,10 +37,11 @@ pub mod data {
}
mod v1 {
use crate::process::process_manager::Platform;
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use crate::process::Platform;
use super::{Deserialize, Serialize, native_model};
fn default_template() -> String {
@ -190,9 +191,9 @@ pub mod data {
use serde_with::serde_as;
use super::{
Deserialize, Serialize, native_model, v1,
};
use crate::runtime_models::Game;
use super::{Deserialize, Serialize, native_model, v1};
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from = v1::Database)]
#[derive(Serialize, Deserialize, Clone, Default)]
@ -274,14 +275,13 @@ pub mod data {
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde, from=v1::DatabaseApplications)]
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, v1::GameVersion>>,
pub installed_game_version: HashMap<String, v1::DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
pub transient_statuses:
HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
@ -302,10 +302,7 @@ pub mod data {
mod v3 {
use std::path::PathBuf;
use super::{
Deserialize, Serialize,
native_model, v2, v1,
};
use super::{Deserialize, Serialize, native_model, v1, v2};
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde, from = v2::Database)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
@ -335,28 +332,73 @@ pub mod data {
}
}
mod v4 {
use std::{collections::HashMap, path::PathBuf};
use drop_library::libraries::LibraryProviderIdentifier;
use drop_native_library::impls::DropNativeLibraryProvider;
use serde_with::serde_as;
use crate::models::data::v3;
use super::{Deserialize, Serialize, native_model, v1, v2};
#[derive(Serialize, Deserialize, Clone)]
pub enum Library {
NativeLibrary(DropNativeLibraryProvider),
}
#[serde_as]
#[derive(Serialize, Deserialize, Default, Clone)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 4, with = native_model::rmp_serde_1_3::RmpSerde, from=v2::DatabaseApplications)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
pub libraries: HashMap<LibraryProviderIdentifier, Library>,
#[serde(skip)]
pub transient_statuses:
HashMap<v1::DownloadableMetadata, v1::ApplicationTransientStatus>,
}
impl From<v2::DatabaseApplications> for DatabaseApplications {
fn from(value: v2::DatabaseApplications) -> Self {
todo!()
}
}
#[native_model(id = 1, version = 4, with = native_model::rmp_serde_1_3::RmpSerde, from = v3::Database)]
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Database {
#[serde(default)]
pub settings: v1::Settings,
pub drop_applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
}
impl From<v3::Database> for Database {
fn from(value: v3::Database) -> Self {
Database {
settings: value.settings,
drop_applications: value.applications.into(),
prev_database: value.prev_database,
}
}
}
}
impl Database {
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
drop_applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
libraries: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: String::new(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: None,
}
}
}
}

View File

@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Platform {
Windows,
Linux,
MacOs,
}
impl Platform {
#[cfg(target_os = "windows")]
pub const HOST: Platform = Self::Windows;
#[cfg(target_os = "macos")]
pub const HOST: Platform = Self::MacOs;
#[cfg(target_os = "linux")]
pub const HOST: Platform = Self::Linux;
pub fn is_case_sensitive(&self) -> bool {
match self {
Self::Windows | Self::MacOs => false,
Self::Linux => true,
}
}
}
impl From<&str> for Platform {
fn from(value: &str) -> Self {
match value.to_lowercase().trim() {
"windows" => Self::Windows,
"linux" => Self::Linux,
"mac" | "macos" => Self::MacOs,
_ => unimplemented!(),
}
}
}
impl From<whoami::Platform> for Platform {
fn from(value: whoami::Platform) -> Self {
match value {
whoami::Platform::Windows => Platform::Windows,
whoami::Platform::Linux => Platform::Linux,
whoami::Platform::MacOS => Platform::MacOs,
_ => unimplemented!(),
}
}
}

View File

@ -0,0 +1,28 @@
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct Game {
pub id: String,
m_name: String,
m_short_description: String,
m_description: String,
// mDevelopers
// mPublishers
m_icon_object_id: String,
m_banner_object_id: String,
m_cover_object_id: String,
m_image_library_object_ids: Vec<String>,
m_image_carousel_object_ids: Vec<String>,
}
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
username: String,
admin: bool,
display_name: String,
profile_picture_object_id: String,
}

View File

@ -0,0 +1,16 @@
[package]
name = "drop-downloads"
version = "0.1.0"
edition = "2024"
[dependencies]
atomic-instant-full = "0.1.0"
drop-database = { path = "../drop-database" }
drop-errors = { path = "../drop-errors" }
# can't depend, cycle
# drop-native-library = { path = "../drop-native-library" }
log = "0.4.22"
parking_lot = "0.12.4"
serde = "1.0.219"
tauri = { version = "2.7.0" }
throttle_my_fn = "0.2.6"

View File

@ -7,14 +7,13 @@ use std::{
thread::{JoinHandle, spawn},
};
use drop_database::models::data::DownloadableMetadata;
use drop_errors::application_download_error::ApplicationDownloadError;
use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter};
use crate::{
database::models::data::DownloadableMetadata,
download_manager::download_manager_frontend::DownloadStatus,
error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
download_manager_frontend::DownloadStatus, events::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}
};
use super::{
@ -30,43 +29,6 @@ use super::{
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
/*
Welcome to the download manager, the most overengineered, glorious piece of bullshit.
The download manager takes a queue of ids and their associated
DownloadAgents, and then, one-by-one, executes them. It provides an interface
to interact with the currently downloading agent, and manage the queue.
When the DownloadManager is initialised, it is designed to provide a reference
which can be used to provide some instructions (the DownloadManagerInterface),
but other than that, it runs without any sort of interruptions.
It does this by opening up two data structures. Primarily is the command_receiver,
and mpsc (multi-channel-single-producer) which allows commands to be sent from
the Interface, and queued up for the Manager to process.
These have been mapped in the DownloadManagerSignal docs.
The other way to interact with the DownloadManager is via the donwload_queue,
which is just a collection of ids which may be rearranged to suit
whichever download queue order is required.
+----------------------------------------------------------------------------+
| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
| WHICH HAS NOT BEEN ACCOUNTED FOR |
+----------------------------------------------------------------------------+
This download queue does not actually own any of the DownloadAgents. It is
simply an id-based reference system. The actual Agents are stored in the
download_agent_registry HashMap, as ordering is no issue here. This is why
appending or removing from the download_queue must be done via signals.
Behold, my madness - quexeky
*/
pub struct DownloadManagerBuilder {
download_agent_registry: HashMap<DownloadableMetadata, DownloadAgent>,
download_queue: Queue,

View File

@ -9,14 +9,11 @@ use std::{
thread::JoinHandle,
};
use drop_database::models::data::DownloadableMetadata;
use drop_errors::application_download_error::ApplicationDownloadError;
use log::{debug, info};
use serde::Serialize;
use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
};
use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent},
util::queue::Queue,

View File

@ -1,12 +1,9 @@
use std::sync::Arc;
use drop_database::models::data::DownloadableMetadata;
use drop_errors::application_download_error::ApplicationDownloadError;
use tauri::AppHandle;
use crate::{
database::models::data::DownloadableMetadata,
error::application_download_error::ApplicationDownloadError,
};
use super::{
download_manager_frontend::DownloadStatus,
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},

View File

@ -0,0 +1,24 @@
use drop_database::models::data::DownloadableMetadata;
use serde::Serialize;
use crate::download_manager_frontend::DownloadStatus;
#[derive(Serialize, Clone)]
pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata,
pub status: DownloadStatus,
pub progress: f64,
pub current: usize,
pub max: usize,
}
#[derive(Serialize, Clone)]
pub struct QueueUpdateEvent {
pub queue: Vec<QueueUpdateEventQueueData>,
}
#[derive(Serialize, Clone)]
pub struct StatsUpdateEvent {
pub speed: usize,
pub time: usize,
}

View File

@ -0,0 +1,7 @@
#![feature(duration_millis_float)]
pub mod download_manager_builder;
pub mod download_manager_frontend;
pub mod downloadable;
pub mod events;
pub mod util;

View File

@ -10,7 +10,7 @@ use std::{
use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle;
use crate::download_manager::download_manager_frontend::DownloadManagerSignal;
use crate::download_manager_frontend::DownloadManagerSignal;
use super::rolling_progress_updates::RollingProgressWindow;

View File

@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex, MutexGuard},
};
use crate::database::models::data::DownloadableMetadata;
use drop_database::models::data::DownloadableMetadata;
#[derive(Clone)]
pub struct Queue {

View File

@ -0,0 +1,14 @@
[package]
name = "drop-errors"
version = "0.1.0"
edition = "2024"
[dependencies]
http = "1.3.1"
humansize = "2.1.3"
reqwest = "0.12.23"
reqwest-websocket = "0.5.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_with = "3.14.0"
tauri-plugin-opener = "2.5.0"
url = "2.5.7"

View File

@ -2,7 +2,7 @@ use serde::Deserialize;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DropServerError {
pub struct ServerError {
pub status_code: usize,
pub status_message: String,
// pub message: String,

View File

@ -8,7 +8,7 @@ use http::StatusCode;
use serde_with::SerializeDisplay;
use url::ParseError;
use super::drop_server_error::DropServerError;
use super::drop_server_error::ServerError;
#[derive(Debug, SerializeDisplay)]
pub enum RemoteAccessError {
@ -18,7 +18,7 @@ pub enum RemoteAccessError {
InvalidEndpoint,
HandshakeFailed(String),
GameNotFound(String),
InvalidResponse(DropServerError),
InvalidResponse(ServerError),
UnparseableResponse(String),
ManifestDownloadFailed(StatusCode, String),
OutOfSync,

View File

@ -0,0 +1,11 @@
[package]
name = "drop-library"
version = "0.1.0"
edition = "2024"
[dependencies]
drop-errors = { path = "../drop-errors" }
http = "*"
reqwest = { version = "*", default-features = false }
serde = { version = "*", default-features = false, features = ["derive"] }
tauri = "*"

View File

@ -0,0 +1,11 @@
pub enum DropLibraryError {
NetworkError(reqwest::Error),
ServerError(drop_errors::drop_server_error::ServerError),
Unconfigured,
}
impl From<reqwest::Error> for DropLibraryError {
fn from(value: reqwest::Error) -> Self {
DropLibraryError::NetworkError(value)
}
}

View File

@ -0,0 +1,30 @@
use crate::libraries::LibraryProviderIdentifier;
pub struct LibraryGamePreview {
pub library: LibraryProviderIdentifier,
pub internal_id: String,
pub name: String,
pub short_description: String,
pub icon: String,
}
pub struct LibraryGame {
pub library: LibraryProviderIdentifier,
pub internal_id: String,
pub name: String,
pub short_description: String,
pub md_description: String,
pub icon: String,
}
impl From<LibraryGame> for LibraryGamePreview {
fn from(value: LibraryGame) -> Self {
LibraryGamePreview {
library: value.library,
internal_id: value.internal_id,
name: value.name,
short_description: value.short_description,
icon: value.icon,
}
}
}

View File

@ -0,0 +1,3 @@
pub mod libraries;
pub mod game;
pub mod errors;

View File

@ -0,0 +1,76 @@
use std::{
fmt::Display,
hash::{DefaultHasher, Hash, Hasher},
};
use http::Request;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tauri::UriSchemeResponder;
use crate::{
errors::DropLibraryError,
game::{LibraryGame, LibraryGamePreview},
};
#[derive(Clone, Serialize, Deserialize)]
pub struct LibraryProviderIdentifier {
internal_id: usize,
name: String,
}
impl PartialEq for LibraryProviderIdentifier {
fn eq(&self, other: &Self) -> bool {
self.internal_id == other.internal_id
}
}
impl Eq for LibraryProviderIdentifier {}
impl Hash for LibraryProviderIdentifier {
fn hash<H: Hasher>(&self, state: &mut H) {
self.internal_id.hash(state);
}
}
impl Display for LibraryProviderIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.name)
}
}
impl LibraryProviderIdentifier {
pub fn str_hash(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish().to_string()
}
}
pub struct LibraryFetchConfig {
pub hard_refresh: bool,
}
pub trait DropLibraryProvider: Serialize + DeserializeOwned + Sized {
fn build(identifier: LibraryProviderIdentifier) -> Self;
fn id(&self) -> &LibraryProviderIdentifier;
fn load_object(
&self,
request: Request<Vec<u8>>,
responder: UriSchemeResponder,
) -> impl Future<Output = Result<(), DropLibraryError>> + Send;
fn fetch_library(
&self,
config: &LibraryFetchConfig,
) -> impl Future<Output = Result<Vec<LibraryGamePreview>, DropLibraryError>> + Send;
fn fetch_game(
&self,
config: &LibraryFetchConfig,
) -> impl Future<Output = Result<LibraryGame, DropLibraryError>> + Send;
fn owns_game(&self, id: &LibraryProviderIdentifier) -> bool {
self.id().internal_id == id.internal_id
}
}

View File

@ -0,0 +1,14 @@
[package]
name = "drop-native-library"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "*"
drop-errors = { path = "../drop-errors" }
drop-library = { path = "../drop-library" }
drop-remote = { path = "../drop-remote" }
log = "*"
serde = { version = "*", features = ["derive"] }
tauri = "*"
url = "*"

View File

@ -1,8 +1,7 @@
use bitcode::{Decode, Encode};
// use drop_database::runtime_models::Game;
use serde::{Deserialize, Serialize};
use crate::games::library::Game;
pub type Collections = Vec<Collection>;
#[derive(Serialize, Deserialize, Debug, Clone, Default, Encode, Decode)]

View File

@ -0,0 +1,11 @@
use drop_database::models::data::{ApplicationTransientStatus, GameDownloadStatus, GameVersion};
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {
pub game_id: String,
pub status: (
Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>,
),
pub version: Option<GameVersion>,
}

View File

@ -0,0 +1,50 @@
use drop_library::{
errors::DropLibraryError, game::{LibraryGame, LibraryGamePreview}, libraries::{DropLibraryProvider, LibraryFetchConfig, LibraryProviderIdentifier}
};
use drop_remote::{fetch_object::fetch_object, DropRemoteContext};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Serialize, Deserialize, Clone)]
pub struct DropNativeLibraryProvider {
identifier: LibraryProviderIdentifier,
context: Option<DropRemoteContext>,
}
impl DropNativeLibraryProvider {
pub fn configure(&mut self, base_url: Url) {
self.context = Some(DropRemoteContext::new(base_url));
}
}
impl DropLibraryProvider for DropNativeLibraryProvider {
fn build(identifier: LibraryProviderIdentifier) -> Self {
Self {
identifier,
context: None,
}
}
fn id(&self) -> &LibraryProviderIdentifier {
&self.identifier
}
async fn load_object(&self, request: tauri::http::Request<Vec<u8>>, responder: tauri::UriSchemeResponder) -> Result<(), DropLibraryError> {
let context = self.context.as_ref().ok_or(DropLibraryError::Unconfigured)?;
fetch_object(context, request, responder).await;
Ok(())
}
async fn fetch_library(
&self,
config: &LibraryFetchConfig
) -> Result<Vec<LibraryGamePreview>, DropLibraryError> {
todo!()
}
async fn fetch_game(&self, config: &LibraryFetchConfig) -> Result<LibraryGame, DropLibraryError> {
todo!()
}
}

View File

@ -0,0 +1,5 @@
//pub mod collections;
//pub mod library;
//pub mod state;
//pub mod events;
pub mod impls;

View File

@ -1,30 +1,34 @@
use std::fs::remove_dir_all;
use std::sync::Mutex;
use std::thread::spawn;
use drop_database::borrow_db_checked;
use drop_database::borrow_db_mut_checked;
use drop_database::models::data::ApplicationTransientStatus;
use drop_database::models::data::Database;
use drop_database::models::data::DownloadableMetadata;
use drop_database::models::data::GameDownloadStatus;
use drop_database::models::data::GameVersion;
use drop_database::runtime_models::Game;
use drop_errors::drop_server_error::ServerError;
use drop_errors::library_error::LibraryError;
use drop_errors::remote_access_error::RemoteAccessError;
use drop_remote::DropRemoteContext;
use drop_remote::auth::generate_authorization_header;
use drop_remote::cache::cache_object;
use drop_remote::cache::cache_object_db;
use drop_remote::cache::get_cached_object;
use drop_remote::cache::get_cached_object_db;
use drop_remote::requests::generate_url;
use drop_remote::utils::DROP_CLIENT_ASYNC;
use drop_remote::utils::DROP_CLIENT_SYNC;
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use tauri::Emitter;
use tauri::Emitter as _;
use crate::AppState;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::Database;
use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
};
use crate::download_manager::download_manager_frontend::DownloadStatus;
use crate::error::drop_server_error::DropServerError;
use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::cache_object_db;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::generate_url;
use crate::remote::utils::DROP_CLIENT_ASYNC;
use crate::remote::utils::DROP_CLIENT_SYNC;
use bitcode::{Decode, Encode};
use crate::events::GameUpdateEvent;
use crate::state::GameStatusManager;
use crate::state::GameStatusWithTransient;
#[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct {
@ -33,53 +37,8 @@ pub struct FetchGameStruct {
version: Option<GameVersion>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct Game {
id: String,
m_name: String,
m_short_description: String,
m_description: String,
// mDevelopers
// mPublishers
m_icon_object_id: String,
m_banner_object_id: String,
m_cover_object_id: String,
m_image_library_object_ids: Vec<String>,
m_image_carousel_object_ids: Vec<String>,
}
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {
pub game_id: String,
pub status: (
Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>,
),
pub version: Option<GameVersion>,
}
#[derive(Serialize, Clone)]
pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata,
pub status: DownloadStatus,
pub progress: f64,
pub current: usize,
pub max: usize,
}
#[derive(serde::Serialize, Clone)]
pub struct QueueUpdateEvent {
pub queue: Vec<QueueUpdateEventQueueData>,
}
#[derive(serde::Serialize, Clone)]
pub struct StatsUpdateEvent {
pub speed: usize,
pub time: usize,
}
pub async fn fetch_library_logic(
state: tauri::State<'_, Mutex<AppState<'_>>>,
context: &DropRemoteContext,
hard_fresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let do_hard_refresh = hard_fresh.unwrap_or(false);
@ -88,15 +47,15 @@ pub async fn fetch_library_logic(
}
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
let response = generate_url(context, &["/api/v1/client/user/library"], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.header("Authorization", generate_authorization_header(context))
.send()
.await?;
if response.status() != 200 {
let err = response.json().await.unwrap_or(DropServerError {
let err = response.json().await.unwrap_or(ServerError {
status_code: 500,
status_message: "Invalid response from server.".to_owned(),
});
@ -106,12 +65,13 @@ pub async fn fetch_library_logic(
let mut games: Vec<Game> = response.json().await?;
let mut handle = state.lock().unwrap();
let mut db_handle = borrow_db_mut_checked();
for game in &games {
handle.games.insert(game.id.clone(), game.clone());
db_handle
.applications
.games
.insert(game.id.clone(), game.clone());
if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle
.applications
@ -127,7 +87,7 @@ pub async fn fetch_library_logic(
}
// We should always have a cache of the object
// Pass db_handle because otherwise we get a gridlock
let game = match get_cached_object_db::<Game>(&meta.id.clone(), &db_handle) {
let game = match get_cached_object_db::<Game>(&meta.id.clone()) {
Ok(game) => game,
Err(err) => {
warn!(
@ -140,14 +100,12 @@ pub async fn fetch_library_logic(
games.push(game);
}
drop(handle);
drop(db_handle);
cache_object("library", &games)?;
Ok(games)
}
pub async fn fetch_library_logic_offline(
_state: tauri::State<'_, Mutex<AppState<'_>>>,
_hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let mut games: Vec<Game> = get_cached_object("library")?;
@ -168,12 +126,10 @@ pub async fn fetch_library_logic_offline(
Ok(games)
}
pub async fn fetch_game_logic(
context: &DropRemoteContext,
id: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let version = {
let state_handle = state.lock().unwrap();
let db_lock = borrow_db_checked();
let metadata_option = db_lock.applications.installed_game_version.get(&id);
@ -187,7 +143,7 @@ pub async fn fetch_game_logic(
.cloned(),
};
let game = state_handle.games.get(&id);
let game = db_lock.applications.games.get(&id);
if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id, &db_lock);
@ -206,15 +162,15 @@ pub async fn fetch_game_logic(
};
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
let response = generate_url(context, &["/api/v1/client/game/", &id], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.header("Authorization", generate_authorization_header(context))
.send()
.await?;
if response.status() == 404 {
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
let offline_fetch = fetch_game_logic_offline(id.clone()).await;
if let Ok(fetch_data) = offline_fetch {
return Ok(fetch_data);
}
@ -229,10 +185,11 @@ pub async fn fetch_game_logic(
let game: Game = response.json().await?;
let mut state_handle = state.lock().unwrap();
state_handle.games.insert(id.clone(), game.clone());
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.games
.insert(id.clone(), game.clone());
db_handle
.applications
@ -255,10 +212,7 @@ pub async fn fetch_game_logic(
Ok(data)
}
pub async fn fetch_game_logic_offline(
id: String,
_state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
pub async fn fetch_game_logic_offline(id: String) -> Result<FetchGameStruct, RemoteAccessError> {
let db_handle = borrow_db_checked();
let metadata_option = db_handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
@ -284,15 +238,19 @@ pub async fn fetch_game_logic_offline(
}
pub async fn fetch_game_version_options_logic(
context: &DropRemoteContext,
game_id: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
let response = generate_url(
context,
&["/api/v1/client/game/versions"],
&[("id", &game_id)],
)?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.header("Authorization", generate_authorization_header(context))
.send()
.await?;
@ -304,19 +262,6 @@ pub async fn fetch_game_version_options_logic(
let data: Vec<GameVersion> = response.json().await?;
let state_lock = state.lock().unwrap();
let process_manager_lock = state_lock.process_manager.lock().unwrap();
let data: Vec<GameVersion> = data
.into_iter()
.filter(|v| {
process_manager_lock
.valid_platform(&v.platform, &state_lock)
.unwrap()
})
.collect();
drop(process_manager_lock);
drop(state_lock);
Ok(data)
}
@ -449,6 +394,7 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
}
pub fn on_game_complete(
context: &DropRemoteContext,
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
@ -460,6 +406,7 @@ pub fn on_game_complete(
let client = DROP_CLIENT_SYNC.clone();
let response = generate_url(
context,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
@ -468,7 +415,7 @@ pub fn on_game_complete(
)?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.header("Authorization", generate_authorization_header(context))
.send()?;
let game_version: GameVersion = response.json()?;
@ -544,48 +491,3 @@ pub fn push_game_update(
)
.unwrap();
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendGameOptions {
launch_string: String,
}
#[tauri::command]
pub fn update_game_configuration(
game_id: String,
options: FrontendGameOptions,
) -> Result<(), LibraryError> {
let mut handle = borrow_db_mut_checked();
let installed_version = handle
.applications
.installed_game_version
.get(&game_id)
.ok_or(LibraryError::MetaNotFound(game_id))?;
let id = installed_version.id.clone();
let version = installed_version.version.clone().unwrap();
let mut existing_configuration = handle
.applications
.game_versions
.get(&id)
.unwrap()
.get(&version)
.unwrap()
.clone();
// Add more options in here
existing_configuration.launch_command_template = options.launch_string;
// Add no more options past here
handle
.applications
.game_versions
.get_mut(&id)
.unwrap()
.insert(version.to_string(), existing_configuration);
Ok(())
}

View File

@ -1,6 +1,4 @@
use crate::database::models::data::{
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus,
};
// use drop_database::models::data::{ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus};
pub type GameStatusWithTransient = (
Option<GameDownloadStatus>,

View File

@ -0,0 +1,18 @@
[package]
name = "drop-process"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
drop-database = { path = "../drop-database" }
drop-errors = { path = "../drop-errors" }
drop-native-library = { path = "../drop-native-library" }
dynfmt = { version = "0.1.5", features = ["curly"] }
log = "0.4.28"
page_size = "0.6.0"
shared_child = "1.1.1"
sysinfo = "0.37.0"
tauri = "2.8.5"
tauri-plugin-opener = "2.5.0"

View File

@ -0,0 +1,4 @@
mod format;
mod process_handlers;
pub mod process_manager;
pub mod utils;

View File

@ -5,13 +5,11 @@ use std::{
sync::LazyLock,
};
use drop_database::{models::data::{Database, DownloadableMetadata, GameVersion}, process::Platform};
use log::{debug, info};
use crate::{
AppState,
database::models::data::{Database, DownloadableMetadata, GameVersion},
process::process_manager::{Platform, ProcessHandler},
};
use crate::process_manager::ProcessHandler;
pub struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
@ -26,7 +24,7 @@ impl ProcessHandler for NativeGameLauncher {
format!("\"{}\" {}", launch_command, args.join(" "))
}
fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool {
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
true
}
}
@ -85,11 +83,8 @@ impl ProcessHandler for UMULauncher {
)
}
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
let Some(ref compat_info) = state.compat_info else {
return false;
};
compat_info.umu_installed
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
UMU_LAUNCHER_EXECUTABLE.is_some()
}
}
@ -123,7 +118,7 @@ impl ProcessHandler for AsahiMuvmLauncher {
#[allow(unreachable_code)]
#[allow(unused_variables)]
fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool {
fn valid_for_platform(&self, _db: &Database, _target: &Platform) -> bool {
#[cfg(not(target_os = "linux"))]
return false;
@ -135,10 +130,6 @@ impl ProcessHandler for AsahiMuvmLauncher {
return false;
}
let Some(ref compat_info) = state.compat_info else {
return false;
};
compat_info.umu_installed
UMU_LAUNCHER_EXECUTABLE.is_some()
}
}

View File

@ -10,30 +10,17 @@ use std::{
time::{Duration, SystemTime},
};
use drop_database::{borrow_db_checked, borrow_db_mut_checked, db::DATA_ROOT_DIR, models::data::{ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, GameDownloadStatus, GameVersion}, process::Platform, DB};
use drop_errors::process_error::ProcessError;
use drop_native_library::{library::push_game_update, state::GameStatusManager};
use dynfmt::Format;
use dynfmt::SimpleCurlyFormat;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use shared_child::SharedChild;
use tauri::{AppHandle, Emitter, Manager};
use tauri::{AppHandle, Emitter};
use tauri_plugin_opener::OpenerExt;
use crate::{
AppState, DB,
database::{
db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked},
models::data::{
ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata,
GameDownloadStatus, GameVersion,
},
},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
process::{
format::DropFormatArgs,
process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher},
},
};
use crate::{format::DropFormatArgs, process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher}};
pub struct RunningProcess {
handle: Arc<SharedChild>,
@ -194,7 +181,6 @@ impl ProcessManager<'_> {
fn fetch_process_handler(
&self,
db_lock: &Database,
state: &AppState,
target_platform: &Platform,
) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> {
Ok(self
@ -204,22 +190,22 @@ impl ProcessManager<'_> {
let (e_current, e_target) = e.0;
e_current == self.current_platform
&& e_target == *target_platform
&& e.1.valid_for_platform(db_lock, state, target_platform)
&& e.1.valid_for_platform(db_lock, target_platform)
})
.ok_or(ProcessError::InvalidPlatform)?
.1)
}
pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result<bool, String> {
pub fn valid_platform(&self, platform: &Platform,) -> Result<bool, String> {
let db_lock = borrow_db_checked();
let process_handler = self.fetch_process_handler(&db_lock, state, platform);
let process_handler = self.fetch_process_handler(&db_lock, platform);
Ok(process_handler.is_ok())
}
pub fn launch_process(
&mut self,
game_id: String,
state: &AppState,
process_manager_lock: &'static Mutex<ProcessManager<'static>>,
) -> Result<(), ProcessError> {
if self.processes.contains_key(&game_id) {
return Err(ProcessError::AlreadyRunning);
@ -304,7 +290,7 @@ impl ProcessManager<'_> {
let target_platform = game_version.platform;
let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?;
let process_handler = self.fetch_process_handler(&db_lock, &target_platform)?;
let (launch, args) = match game_status {
GameDownloadStatus::Installed {
@ -385,22 +371,17 @@ impl ProcessManager<'_> {
);
let wait_thread_handle = launch_process_handle.clone();
let wait_thread_apphandle = self.app_handle.clone();
let wait_thread_game_id = meta.clone();
spawn(move || {
let result: Result<ExitStatus, std::io::Error> = launch_process_handle.wait();
let app_state = wait_thread_apphandle.state::<Mutex<AppState>>();
let app_state_handle = app_state.lock().unwrap();
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
let mut process_manager_handle = process_manager_lock.lock().unwrap();
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
// As everything goes out of scope, they should get dropped
// But just to explicit about it
drop(process_manager_handle);
drop(app_state_handle);
});
self.processes.insert(
@ -415,51 +396,6 @@ impl ProcessManager<'_> {
}
}
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Platform {
Windows,
Linux,
MacOs,
}
impl Platform {
#[cfg(target_os = "windows")]
pub const HOST: Platform = Self::Windows;
#[cfg(target_os = "macos")]
pub const HOST: Platform = Self::MacOs;
#[cfg(target_os = "linux")]
pub const HOST: Platform = Self::Linux;
pub fn is_case_sensitive(&self) -> bool {
match self {
Self::Windows | Self::MacOs => false,
Self::Linux => true,
}
}
}
impl From<&str> for Platform {
fn from(value: &str) -> Self {
match value.to_lowercase().trim() {
"windows" => Self::Windows,
"linux" => Self::Linux,
"mac" | "macos" => Self::MacOs,
_ => unimplemented!(),
}
}
}
impl From<whoami::Platform> for Platform {
fn from(value: whoami::Platform) -> Self {
match value {
whoami::Platform::Windows => Platform::Windows,
whoami::Platform::Linux => Platform::Linux,
whoami::Platform::MacOS => Platform::MacOs,
_ => unimplemented!(),
}
}
}
pub trait ProcessHandler: Send + 'static {
fn create_launch_process(
&self,
@ -470,5 +406,5 @@ pub trait ProcessHandler: Send + 'static {
current_dir: &str,
) -> String;
fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool;
fn valid_for_platform(&self, db: &Database, target: &Platform) -> bool;
}

View File

@ -1,10 +1,8 @@
use std::{path::PathBuf, sync::Arc};
use std::{io, path::PathBuf, sync::Arc};
use futures_lite::io;
use drop_errors::application_download_error::ApplicationDownloadError;
use sysinfo::{Disk, DiskRefreshKind, Disks};
use crate::error::application_download_error::ApplicationDownloadError;
pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownloadError> {
let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::nothing().with_storage());

View File

@ -0,0 +1,20 @@
[package]
name = "drop-remote"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "0.6.7"
chrono = "0.4.42"
drop-consts = { path = "../drop-consts" }
drop-errors = { path = "../drop-errors" }
droplet-rs = "0.7.3"
gethostname = "1.0.2"
hex = "0.4.3"
http = "1.3.1"
log = "0.4.28"
md5 = "0.8.0"
reqwest = "0.12.23"
serde = { version = "1.0.220", features = ["derive"] }
tauri = "2.8.5"
url = "2.5.7"

View File

@ -0,0 +1,156 @@
use std::{collections::HashMap, env, sync::Mutex};
use chrono::Utc;
use drop_errors::{drop_server_error::ServerError, remote_access_error::RemoteAccessError};
use droplet_rs::ssl::sign_nonce;
use gethostname::gethostname;
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}, DropRemoteAuth, DropRemoteContext
};
use super::requests::generate_url;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CapabilityConfiguration {}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct InitiateRequestBody {
name: String,
platform: String,
capabilities: HashMap<String, CapabilityConfiguration>,
mode: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct HandshakeRequestBody {
client_id: String,
token: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct HandshakeResponse {
private: String,
certificate: String,
id: String,
}
pub fn generate_authorization_header(context: &DropRemoteContext) -> String {
let auth = if let Some(auth) = &context.auth {
auth
} else {
return "".to_owned();
};
let nonce = Utc::now().timestamp_millis().to_string();
let signature = sign_nonce(auth.private.clone(), nonce.clone()).unwrap();
format!("Nonce {} {} {}", auth.client_id, nonce, signature)
}
pub async fn fetch_user(context: &DropRemoteContext) -> Result<Vec<u8>, RemoteAccessError> {
let response =
make_authenticated_get(context, generate_url(context, &["/api/v1/client/user"], &[])?).await?;
if response.status() != 200 {
let err: ServerError = response.json().await?;
warn!("{err:?}");
if err.status_message == "Nonce expired" {
return Err(RemoteAccessError::OutOfSync);
}
return Err(RemoteAccessError::InvalidResponse(err));
}
response
.bytes()
.await
.map_err(std::convert::Into::into)
.map(|v| v.to_vec())
}
pub async fn recieve_handshake_logic(
context: &mut DropRemoteContext,
path: String,
) -> Result<(), RemoteAccessError> {
let path_chunks: Vec<&str> = path.split('/').collect();
if path_chunks.len() != 3 {
// app.emit("auth/failed", ()).unwrap();
return Err(RemoteAccessError::HandshakeFailed(
"failed to parse token".to_string(),
));
}
let client_id = path_chunks.get(1).unwrap();
let token = path_chunks.get(2).unwrap();
let body = HandshakeRequestBody {
client_id: (*client_id).to_string(),
token: (*token).to_string(),
};
let endpoint = generate_url(context, &["/api/v1/client/auth/handshake"], &[])?;
let client = DROP_CLIENT_ASYNC.clone();
let response = client.post(endpoint).json(&body).send().await?;
debug!("handshake responsded with {}", response.status().as_u16());
if !response.status().is_success() {
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
}
let response_struct: HandshakeResponse = response.json().await?;
let web_token = {
let header = generate_authorization_header(context);
let token = client
.post(generate_url(context, &["/api/v1/client/user/webtoken"], &[])?)
.header("Authorization", header)
.send()
.await
.unwrap();
token.text().await.unwrap()
};
context.auth = Some(DropRemoteAuth {
private: response_struct.private,
cert: response_struct.certificate,
client_id: response_struct.id,
web_token: web_token,
});
Ok(())
}
pub fn auth_initiate_logic(context: &DropRemoteContext, mode: String) -> Result<String, RemoteAccessError> {
let hostname = gethostname();
let endpoint = generate_url(context, &["/api/v1/client/auth/initiate"], &[])?;
let body = InitiateRequestBody {
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
platform: env::consts::OS.to_string(),
capabilities: HashMap::from([
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
]),
mode,
};
let client = DROP_CLIENT_SYNC.clone();
let response = client.post(endpoint.to_string()).json(&body).send()?;
if response.status() != 200 {
let data: ServerError = response.json()?;
error!("could not start handshake: {}", data.status_message);
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
let response = response.text()?;
Ok(response)
}

View File

@ -5,18 +5,18 @@ use std::{
time::SystemTime,
};
use crate::{
database::{db::borrow_db_checked, models::data::Database},
error::remote_access_error::RemoteAccessError,
};
use bitcode::{Decode, DecodeOwned, Encode};
use drop_consts::CACHE_DIR;
use drop_errors::remote_access_error::RemoteAccessError;
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
#[macro_export]
macro_rules! offline {
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline {
// TODO add offline mode back
// || $var.lock().unwrap().status == AppStatus::Offline
async move { if drop_database::borrow_db_checked().settings.force_offline {
$func2( $( $arg ), *).await
} else {
$func1( $( $arg ), *).await
@ -57,36 +57,33 @@ fn delete_sync(base: &Path, key: &str) -> io::Result<()> {
}
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
cache_object_db(key, data, &borrow_db_checked())
cache_object_db(key, data)
}
pub fn cache_object_db<D: Encode>(
key: &str,
data: &D,
database: &Database,
) -> Result<(), RemoteAccessError> {
let bytes = bitcode::encode(data);
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
write_sync(&CACHE_DIR, key, bytes).map_err(RemoteAccessError::Cache)
}
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
get_cached_object_db::<D>(key, &borrow_db_checked())
get_cached_object_db::<D>(key)
}
pub fn get_cached_object_db<D: DecodeOwned>(
key: &str,
db: &Database,
) -> Result<D, RemoteAccessError> {
let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
let bytes = read_sync(&CACHE_DIR, key).map_err(RemoteAccessError::Cache)?;
let data =
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
Ok(data)
}
pub fn clear_cached_object(key: &str) -> Result<(), RemoteAccessError> {
clear_cached_object_db(key, &borrow_db_checked())
clear_cached_object_db(key)
}
pub fn clear_cached_object_db(
key: &str,
db: &Database,
) -> Result<(), RemoteAccessError> {
delete_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
delete_sync(&CACHE_DIR, key).map_err(RemoteAccessError::Cache)?;
Ok(())
}

View File

@ -1,15 +1,16 @@
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Request};
use log::warn;
use tauri::UriSchemeResponder;
use crate::{database::db::DatabaseImpls, remote::utils::DROP_CLIENT_ASYNC, DB};
use crate::{requests::generate_url, utils::DROP_CLIENT_ASYNC, DropRemoteContext};
use super::{
auth::generate_authorization_header,
cache::{ObjectCache, cache_object, get_cached_object},
};
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
pub async fn fetch_object(context: &DropRemoteContext, request: Request<Vec<u8>>, responder: UriSchemeResponder) {
// Drop leading /
let object_id = &request.uri().path()[1..];
@ -21,9 +22,9 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
return;
}
let header = generate_authorization_header();
let header = generate_authorization_header(context);
let client = DROP_CLIENT_ASYNC.clone();
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
let url = generate_url(context, &["/api/v1/client/object", object_id], &[]).expect("failed to generated object url");
let response = client.get(url).header("Authorization", header).send().await;
if response.is_err() {

View File

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
use url::Url;
pub mod auth;
pub mod cache;
pub mod fetch_object;
pub mod requests;
pub mod utils;
#[derive(Serialize, Deserialize, Clone)]
struct DropRemoteAuth {
private: String,
cert: String,
client_id: String,
web_token: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct DropRemoteContext {
base_url: Url,
auth: Option<DropRemoteAuth>,
}
impl DropRemoteContext {
pub fn new(base_url: Url) -> Self {
DropRemoteContext { base_url, auth: None }
}
}

Some files were not shown because too many files have changed in this diff Show More