Compare commits

..

7 commits

Author SHA1 Message Date
quexeky
9fab0af4e8
fix: Removed unnecessary nightly feature try_trait_v2 2025-01-28 22:48:26 +11:00
quexeky
3968a61786
fix(downloads): Fix rearranging download queue throwing error 2025-01-25 23:20:31 +11:00
quexeky
8861fe4e3d
chore: Version bump 2025-01-25 19:46:58 +11:00
quexeky
f5bd12b43a
Merge develop into main (#25)
* chore(process manager): refactor for generic way to implement cross
platform launchers

* feat(game): game uninstalling & partial compat

* chore(metadata): update metadata

* feat(errors): better download manager errors + modal

* feat(process): better process management, including running state

* feat(downloads): lockless tracking of downloaded chunks

* fix(sign on): add message about nonce expiration

* feat(download ui): add speed and time remaining information

closes #7

Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>

* chore: Ran cargo clippy

Signed-off-by: quexeky <git@quexeky.dev>

* fix(auth initiate): add better error message

* feat(auth): offer manual signin

* feat(install modal): add note about more install dirs

* fix(install flow): clear stale data before requesting new

* Delete pages/library.vue

* Add files via upload

* adds nvm rc!

* feat(install modal): add note about more install dirs

* fix(install flow): clear stale data before requesting new

* Delete pages/library.vue

* Add files via upload

* fix(library page): fix install button

* fix(process): fix poorly designed parsing for executables with spaces

* fix(scrollbars): fix ugly scrollbars on edge webview

* feat(Compat): Implemented spawning with umu (using umu-wrapper-lib)

Signed-off-by: quexeky <git@quexeky.dev>

* feat(process manager): Game kill tauri command

Signed-off-by: quexeky <git@quexeky.dev>

* fix(deep links): Re-enabled deep links

Signed-off-by: quexeky <git@quexeky.dev>

* feat(process): shared child with stop command

* squash(autostart): added adenmgb's autostart feature

Squashed commit of the following:

commit 085cd9481dee748ee84c1a8f9dcd8ef0b01105ab
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:29:41 2024 +1030

    Update lib.rs for the DB sync of autostart

commit 86f2fb19bde3933ec6b5cd82701de0d306121659
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:29:13 2024 +1030

    Update db.rs to accomidate the settings sync

commit ece11e7581c9aef55588b2e829379b1224021a45
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:27:48 2024 +1030

    Update autostart.rs to include DB

commit 7ea8a24fdc2ff98379694ce8e347a40fcfd5fea3
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:38 2024 +1030

    Add files via upload

commit af2f232d94fa449d9e20df737ce81ebded87fd50
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:09 2024 +1030

    Delete src-tauri/Cargo.toml

commit 5d27b65612457de6eb30835d5423b9d96fd5a596
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:15:42 2024 +1030

    Add files via upload

commit 2eea7b97a876e23cc0d6daec37f1b75af9bf3ae2
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:15:31 2024 +1030

    Delete src-tauri/src/lib.rs

commit 9a635a10d1340f86c74812113284b115b34b9bbe
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:14:49 2024 +1030

    Add files via upload

commit 2fb049531a082fbdd217aba694819b7a3f954a55
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:13:37 2024 +1030

    Add files via upload

commit ea1be4d7505a9ab16bda338491c0ec313d0bc586
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:13:20 2024 +1030

    Delete pages/settings/index.vue

* fix(download manager): fix incorrect error assumptions & update types

* feat(account settings): Add signout functionality (#16)

* Create account.vue with logout button

* Update auth.rs to add signout command

* Update lib.rs to pass sign_out command to frontend

* feat(settings): add debug page

* Create debug.rs

* Update settings.vue to add tab for debug

* Update main.scss to add light theme

* Update interface.vue to add light mode

* Create debug.vue

* Update debug.vue too add open log button

* Update lib.rs

* Update debug.rs

* Update debug.rs

* Update lib.rs

* Update lib.rs

* Update debug.rs

* Update debug.vue

* fix(debug): refactor and cleanup

* revert(theme): revert light theming

---------

Co-authored-by: DecDuck <declanahofmeyr@gmail.com>

* feat(library ui): add installed ui in the library menu

* chore(tool manager): Progress on adding tools

Going to try changing around the download manager to take a generic trait rather than specifically for game downloads

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Moved download manager to separate directory

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Added Downloadable trait and replaced references to GameDownloadAgent

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Renamed most instances of "game" outside of actual game downloads

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Renamed GameDonwloadError to ApplicationDownloadError and moved

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Some easy cleanup of the download manager

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Ensure that Downloadable is also send and sync

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Moved manifest and stored_manifest to download_manager

Signed-off-by: quexeky <git@quexeky.dev>

* Revert "refactor(download manager): Moved manifest and stored_manifest to download_manager"

This reverts commit 8db2393346.

* chore(tool manager): Added ToolDownloadAgent

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Added manage_queue_signal

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Added manage_go_signal command

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Removed all references to anything outside of the DownloadManager

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Fully separate & generic download manager

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Removed Arc requirement for DownloadableMetadata

Signed-off-by: quexeky <git@quexeky.dev>

* feat(download manager): Added generic download manager

Signed-off-by: quexeky <git@quexeky.dev>

* fix(game launcher): Renamed game_id to id

Signed-off-by: quexeky <git@quexeky.dev>

* fix(uninstalling): Re-enabled uninstalling apps

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(downloads): Moved all files relevant to game downloads to their own directory

Signed-off-by: quexeky <git@quexeky.dev>

* fix(kill game): Re-enabled killing games

Signed-off-by: quexeky <git@quexeky.dev>

* feat(recovery): Added database recovery

Signed-off-by: quexeky <git@quexeky.dev>

* feat(database): Added database corruption dialog

Signed-off-by: quexeky <git@quexeky.dev>

* chore(README): Updated README.md

Signed-off-by: quexeky <git@quexeky.dev>

* perf(game downloads): Moved some variable declarations outside of the spawned download thread

Signed-off-by: quexeky <git@quexeky.dev>

* fix(game downloads): Accidentally was attempting to lock onto something that was already in scope

Signed-off-by: quexeky <git@quexeky.dev>

* fix(db): Added Settings component

Signed-off-by: quexeky <git@quexeky.dev>

* refactor: Ran cargo clippy & fmt

Signed-off-by: quexeky <git@quexeky.dev>

* chore: More cleanup after cargo clippy

Also added some type efficiency improvements (using references where possible and added SliceDeque crate)

Signed-off-by: quexeky <git@quexeky.dev>

* feat(settings): Added max_download_threads setting and separated settings from db

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs

Signed-off-by: quexeky <git@quexeky.dev>

* chore: General cleanup

- Changed some info!() statements to debug!() and warn!()
- Removed most Turbofish syntax cases
- Removed InvalidCodeError and replaced it with InvalidResponse

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Removed tests/

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Removed tools/

Signed-off-by: quexeky <git@quexeky.dev>

* chore: More refining info!() statements

Signed-off-by: quexeky <git@quexeky.dev>

* feat(download manager): Added UI to change download threads

Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>
Signed-off-by: quexeky <git@quexeky.dev>

* fix(metadata): update routes for new server

* fix(handle invalid database): use set_file_name instead of pushing to
strings

* refactor(compat): remove unnecessary compat code (#20)

* Delete pages/settings/compatibility.vue

* Update settings.vue

* Update debug.vue

* Update lib.rs

* Update compat.rs

* feat(debug): use shift or DEBUG RUST_LOG to show Debug Info

* Update settings.vue to have a conditional debug page

* Update debug.rs to add RUST_LOG status fetching

* Implement better error system and segregate errors and commands (#23)

* chore: Progress on amend_settings command

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Progress on better error handling with segragation of files

* chore: Progress on amend_settings command

Signed-off-by: quexeky <git@quexeky.dev>

* chore(commands): Separated commands under each subdirectory into respective commands.rs files

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Almost all errors and commands have been segregated

* chore(errors): Added drop server error

Signed-off-by: quexeky <git@quexeky.dev>

* feat(core): Update to using nightly compiler

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): More progress on error handling

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Implementing Try and FromResidual for UserValue

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(errors): Segregated errors and commands from code, and made commands return UserValue struct

Signed-off-by: quexeky <git@quexeky.dev>

* fix(errors): Added missing files

* chore(errors): Convert match statement to map_err

* feat(settings): Implemented settings editing from UI

* feat(errors): Clarified return values from retry_connect command

* chore(errors): Moved autostart commands to autostart.rs

* chore(process manager): Converted launch_process function for games to use game_id

---------

Signed-off-by: quexeky <git@quexeky.dev>

* fix(settings): Broken command invoke logic in settings/downloads.vue

* feat(logging): Added line numbers to file logging and highlighting to console

* chore(progress): Added rolling_progress_updates.rs

Signed-off-by: quexeky <git@quexeky.dev>

* chore(exit): Progress on cleanup and exit

* chore(downloads): Progress on terminator

* chore: Progress on rolling progress window

* feat(progress): Added rolling progress window

Still needs tweaks on specific timings, as well as cleanup

* refactor(remote): Created separate function to generate requests

* fix(install ui): stop loading on error

* fix: fix other metadata endpoints

* feat(errors): Using SerializeDisplay for better error management with Result

* chore: Update .gitlab-ci.yml

* refactor(logging): Using more appropriate logging statements

Still probably needs some work, but that's enough for now

* chore(logging): Imported appropriate logging macros

* Revert "chore: Update .gitlab-ci.yml"

This reverts commit fc6bab9381.

* feat(settings): Allow settings to update UI using fetch_settings command

* style(logging): Ensured that all logs start with lowercase capital and have no trailing punctuation

* fix(download manager): don't crash download manager if multiple errors
come in

* feat(downloads): re-enable checksums

* fix(logs): add file & line to console logs

* fix(ui): modal stack doesn't cover whole app

* feat(database): Ensure that any database issues are resolved by standalone functions

Functions are as follows:
- save_db()
- borrow_db_checked()
- borrow_db_mut_checked()

* chore: Ran cargo clippy & cargo fmt

* fix: assorted fixes

* fix(download agent): fixed completed indexes

* fix: Adding usize to completed_contexts_lock instead of &usize

* fix(game downloads): Added error handling for chunk request errors

* chore: Apply stashed changes

* feat(games): Added multi-argument game launch and setup support

* fix: Games not launching due to string semantics

* build: Version bump & appimage build

* chore: Update .gitlab-ci.yml

* Update .gitlab-ci.yml

* Update .gitlab-ci.yml with artifacts

* feat(settings): Made save button include user feedback & only allow numeric characters

* fix(library): Added "LIbrary Failed to Update" content to recover from library load fail

* fix(logging): Restored RUST_LOG env functionality

* Update changelog.md

---------

Signed-off-by: quexeky <git@quexeky.dev>
Signed-off-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>
Co-authored-by: seethruhead <shane.keulen@gmail.com>
2025-01-25 18:49:54 +11:00
quexeky
6e4ac4ad83
Update issue templates 2025-01-21 10:17:46 +11:00
quexeky
40c4e8a71c
Update .gitlab-ci.yml file 2025-01-21 10:17:46 +11:00
quexeky
4ad688da14
refactor(downloads): Replaced static usage with const 2025-01-21 10:17:30 +11:00
57 changed files with 1128 additions and 671 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Arch Linux, Windows]
- App Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -3,7 +3,7 @@ stages:
build-linux:
stage: build
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rustlang/rust:nightly
script:
- apt-get update -y
- apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y
@ -13,9 +13,10 @@ build-linux:
- yarnpkg tauri build
- cp src-tauri/target/release/bundle/deb/*.deb .
- cp src-tauri/target/release/bundle/rpm/*.rpm .
- cp src-tauri/target/release/bundle/appimage/*.AppImage .
artifacts:
paths:
- "*.{deb,rpm}"
- "*.{deb,rpm,AppImage}"
build-windows:
stage: build

14
app.vue
View file

@ -1,8 +1,8 @@
<template>
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
</NuxtLayout>
<ModalStack />
</template>
<script setup lang="ts">
@ -20,10 +20,18 @@ import {
const router = useRouter();
const state = useAppState();
state.value = JSON.parse(await invoke("fetch_state"));
try {
state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
router.beforeEach(async () => {
state.value = JSON.parse(await invoke("fetch_state"));
try {
state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
});
setupHooks();

View file

@ -1,5 +1,292 @@
## Release 0.2.0-beta
### Fixes
- Re-enabled killing games #005bab2
- fixed queue manipulation and waiting for downloads #01260f0
- fix logic error in detecting dir #04368ff
- absolute executable invoke #17759c4
- don't crash download manager if multiple errors come in #21204de
- clear stale data before requesting new #327628b
- fixed completed indexes #39f2ebd
- add file & line to console logs #4d8eadc
- Games not launching due to string semantics #4ef49cc
- Added error handling for chunk request errors #4fc0855
- Chunk counting logic error #5ba151f
- modal stack doesn't cover whole app #5db9ae5
- use set_file_name instead of pushing to strings #60d0a48
- use of completed signal, and pause/resuming #64d7f64
- add message about nonce expiration #6a8d0af
- Added "LIbrary Failed to Update" content to recover from library load fail #76bae3d
- Restored RUST_LOG env functionality #7a0cf4f
- initialise doesn't recreate default install dir #7a3841b
- update routes for new server #7ab53f3
- use vendored flag #7c8089e
- fix poorly designed parsing for executables with spaces #7c90d2b
- assorted fixes #89ea34c
- Added Settings component #8aad64f
- windows build #8d9234f
- fix ugly scrollbars on edge webview #95f2174
- windows shadow #9a8cc59
- add better error message #9af0d08
- Broken command invoke logic in settings/downloads.vue #9e29aa7
- Accidentally was attempting to lock onto something that was already in scope #9e82a0b
- fix incorrect error assumptions & update types #a17311a
- Re-enabled uninstalling apps #a56ee25
- types #af056c0
- fix other metadata endpoints #c2f54c1
- Re-enabled deep links #c3f6222
- added console as an appender #d12bf15
- remove unnecessary unstable feature #d5ac1b0
- fix install button #d7b0302
- stop loading on error #d83aae6
- use unix timestamp to avoid invalid characters in filename #dafce24
- Renamed game_id to id #dceaa56
- use chrono library to generate timestamps #e22e6d8
- clear stale data before requesting new #e72662c
- fix scrollbars on edge webview #f09605a
- update readme instructions #f0c47d8
- Adding usize to completed_contexts_lock instead of &usize #f508186
### Features
- Game kill tauri command #01e6162
- add debug page #02f8591
- Add signout functionality (#16) #0a0d9d6
- queue and library UIs #0a20139
- add note about more install dirs #139bc0c
- Using SerializeDisplay for better error management with Result #170fde5
- add pre-launch log to file #17f8d76
- Added option to change root directory #1aa52c0
- add speed and time remaining information #1f899ec
- lockless tracking of downloaded chunks #2183585
- quit button #239b8d5
- use shift or DEBUG RUST_LOG to show Debug Info #245a84d
- Added database corruption dialog #25ba200
- only allow downloads for supported platforms #269dcbb
- add installed ui in the library menu #2c8164e
- added file-based logging #2d4a7e8
- automatically fetch remote data if not available #2dedfbb
- Added database recovery #32ae7d5
- ability to add more download dirs #384f7a5
- re-enable checksums #3ca87fc
- background processes and close/open menu #3d60fd5
- launch games with log files #3f71149
- Download cancelling #450bca9
- refactoring and error message #469a2d6
- Added UI to change download threads #4e93eb4
- Made save button include user feedback & only allow numeric characters #53234d2
- download widget and queue fix #532d13e
- Pausing and resuming game downloads #55b7921
- Allow settings to update UI using fetch_settings command #5bb04da
- temporary queue ui and flamegraph instructions #5cbeb3b
- Added DownloadThreadControl struct #5e05e68
- Added max_download_threads setting and separated settings from db #5ea47d7
- Added generic download manager #6159319
- Added AgentInterfaceData to get information about all downloads in queue #63c3cc1
- debug queue interface #671d45f
- reduce scope of download agent #6a38ea3
- Added multi-argument game launch and setup support #6ad3837
- shared child with stop command #6b96e40
- Added function to take and set any game state #6bc6482
- Added line numbers to file logging and highlighting to console #7c3140e
- Separated chunk updates into individual counters #7d3c601
- Ensure that any database issues are resolved by standalone functions #7d4651d
- ui to install games #8670bca
- Implemented spawning with umu (using umu-wrapper-lib) #88b2505
- offer manual signin #949acfc
- better process management, including running state #a135b13
- Added Download Manager #a1ada07
- retry connnection on server unavailable #a53d838
- finish download dir CRUD interface #a580a46
- better download manager errors + modal #ad92dbe
- syncs state to disk to persist across reboots #b556842
- prevent default context menu and emit event on elements #c560656
- initial creation and logo update #d9a51cf
- Added manifest.json utility for persistent download progress #d9d0122
- game uninstalling & partial compat #dd7f567
- combined db and download interface improvements #de52dac
- update db state with ui and emit events #e4df4eb
- Generic function to set download state #f10d92d
- Convert DownloadThreadControlFlag to AtomicBool #f25bfed
- add note about more install dirs #f4ac1c8
- Added rolling progress window #fd30b3e
### Other Changes
- quexeky <git@quexeky.dev>
- Convert DATA_ROOT_DIR to Mutex #00b7179
- Converting DB access to a trait #01b092c
- Updated changelog #022330b
- Progress on cleanup and exit #0381b8b
- library ui #03fa364
- Scoping changes and removing qualifications #046ba64
- Moved all files relevant to game downloads to their own directory #06d1e9e
- SLowly integrating game_download into the FE. Started with using the manifest minimal example in the server (#1) #07379b2
- Ran cargo clippy & moved DownloadManagerInterface #075d6ec
- Made logging systems match #0a1dddf
- Some easy cleanup of the download manager #0a2ac25
- client now fetches user information from Drop server #0c0cfeb
- Included in AppStatus (Also trying to link to Issue #1)
- Accidentally serialized AppStatus and broke everything :/ #10791ed
- Removed debugging statements #10c8344
- Wrappers are the bane of my existence. Also here's the download cancelling logic. #13df631
- Merge branch 'error-handling' #1520471
- Updated README.md #165a967
- Removed unnecessary dependencies #1724449
- merge(download-manager) -> 'main' #172d6b0
- More refactoring and renaming camelCase struct definitions to snake_case #1742793
- General cleanup #182361e
- Delete pages/library.vue #1861659
- progress on more precise download control #18b9149
- Allowing some dead code features because they are there for future use (potentially) #191e62c
- Ensure that Downloadable is also send and sync #1a89135
- I think that download queuing is working #1ab61c8
- auth initiate, database and more #22b1aee
- Update .gitlab-ci.yml" #2307704
- More fleshing out on how specifically game downloads will work (#1) #23137dd
- Removed utils.rs #270bc8b
- Fixing some references to "id" vs "game_id" #27e5a8e
- More cleanup after cargo clippy #2822b7a
- Updated contributing link #2aa5b9c
- More fleshing out on how specifically game downloads will work #2b90de9
- Cleaning up downloads playing and pausing #2c7b5fb
- fixed multi-chunk downloads #2ec351f
- Clippy refactoring #2efe304
- remove unpacker mod statement #32067c0
- Progress on adding tools #3299c71
- Fixed bug with bad initial loading into store instead of auth #3923acf
- add nvm rc #3ccd444
- partial download manager #3dbf5ab
- Update .gitlab-ci.yml with artifacts #3e10f17
- Removed tools/ #3eda979
- Downloads should be fixed now #403ca65
- transient vs synced state now defined #42c0198
- added adenmgb's autostart feature #472eb1d
- better download defaults #4779383
- Progress on downloads. Currently working on parsing functions to be run asynchronously #496c6a5
- Ran cargo clippy & cargo fmt #4983b25
- handshakes #4bb33c8
- Convert DOWNLOAD_MAX_THREADS to const #4fc13a1
- Merge branch 'downloads' #50ed841
- Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs #50f37fd
- Added time debugging and fixed logging formatting #5243694
- Clippy changes #553bc37
- Queue is running game downloads sequentially now #5564d23
- migrate to new droplet ca system #556898f
- Add LICENSE #57a5737
- ran cargo clippy & cargo fmt #5e3d26b
- my own take on some BASED design decisions #5ed0833
- cleanup and game UI beginnings #5ef6b8e
- Progress on terminator #5f5cbd0
- Implement better error system and segregate errors and commands (#23) #604d5b5
- moved to completed index arr to help serialization #64ebc19
- Ran cargo clippy & cargo fmt #653717e
- Removed all references to anything outside of the DownloadManager #6568faa
- Merge remote-tracking branch 'origin/main' #68ca4a7
- swap file name and to binary encoding #694f2fd
- chore(polish & cleanup) #6cc0c67
- Update .gitlab-ci.yml #6d7630e
- Moved some variable declarations outside of the spawned download thread #6ea4cf2
- Encoding game IDs and versions #6ef444e
- restructing and renaming #7049673
- Converted to md5 #706f525
- Merge branch 'main' into downloads #714b968
- Semantic naming changes #725f16b
- Abstracted queue system #76b0975
- Moved manifest and stored_manifest to download_manager" #78149bb
- README update #78fc668
- Ensured everything is serializing/deserializing to camelCase #7a95b7f
- fixed some of quexeky's BASED design decisions #7e3da04
- Progress checker works #7fec00d
- Progress on refactoring and abiding by cargo clippy #816b427
- Added GAME_PAUSE_CHECK_INTERVAL value #8204795
- Ran cargo clippy & fmt #82804eb
- update metadata #85a0899
- Renamed most instances of "game" outside of actual game downloads #881fcc6
- Debugging & starting work on parsing manifest #89d2814
- slight ui/ux fixes and updates to auth protocol #8a2d23d
- Removed Arc requirement for DownloadableMetadata #8be1dd4
- compliant with new APIs #8f6f184
- Ran cargo clippy & cargo fmt #9272970
- Added rolling_progress_updates.rs #9369ff1
- Add files via upload #93b8b83
- More refining info!() statements #94cf678
- fixed windows issues #959dad3
- Starting p2p progress #97bb1fa
- Game downloads from the client are working (multithreaded) by parsing in gameID, GameVersion, and maxThreads from FE (#1) #984472e
- Version bump & appimage build #9897698
- Some progress on thread terminations #99beca4
- rename files to what they contain #99c8b39
- Created separate function to generate requests #9a184a8
- cleanup of lib and toml #9b1cfa7
- refactor for generic way to implement cross platform launchers #9ea2aa4
- Updated logging format #a213765
- fix(windows build) #a24cc8a
- Added ToolDownloadAgent #a2e63aa
- copy direct to disk #a628fc1
- Moved manifest and stored_manifest to download_manager #a846eed
- adds nvm rc! #a881d8e
- Reordered DownloadThreadControlFlag to agree with From<bool> #ab606e8
- ci/cd and patches for windows builds #ac1c3b6
- patch for not draggable windows during setup #ac66b20
- another stage of client authentication #ae4c65b
- Renamed GameDonwloadError to ApplicationDownloadError and moved #aed58e4
- Progress on write speeds & added debug statements #b065e10
- Updated logging #b3963b6
- Created file settings.rs #b47b7ea
- Added Downloadable trait and replaced references to GameDownloadAgent #b4d70a3
- Update .gitlab-ci.yml #b6a54c0
- Moved download manager to separate directory #b6c64e5
- Ran cargo fmt #b8cf44c
- Imported appropriate logging macros #b99ff67
- Merge branch 'main' into download-manager #bb60942
- Ran cargo clippy & cargo fmt #bd3deac
- beginnings of game state management #bf46dec
- Update Cargo.toml #c1fb39e
- migrated unpacking to rust zstd to conform with droplet #c46c54b
- More progress on checksums #c51e761
- Delete pages/library.vue #c722a54
- Merge branch 'downloads' (again) #c748aec
- migrate to nuxt and groundwork #c957744
- More debugging because apparently checksums are the bane of my existence. But it works and I was just an idiot #c9d9d2e
- Fully separate & generic download manager #cac612b
- Progress on rolling progress window #cf19477
- Ensured that all logs start with lowercase capital and have no trailing punctuation #cfc9d13
- Validated that loading data works #d21b1d2
- Mostly finished with checksums. Just merging main in at the same time #d39e7cb
- Ran cargo clippy #dcb1564
- Add files via upload #dcb2c0f
- Theoretically adding queue support and optimistic manifest downloading (#1). Needs tests when actual functions are implemented #dcd8fa8
- Merge remote-tracking branch 'origin/downloads' into downloads #dd23ca8
- Debugging line #ddc585d
- Re-enabled closing the window and some more renaming #defba51
- drop no longer freaks out if server is unavailable on startup #df88395
- Apply stashed changes #e0ea8c9
- Merge remote-tracking branch 'origin/downloads' into downloads #e4e605b
- convert to more sensible permission schema #e504c00
- Update on GameDownload #e71e4cf
- reorganisation, cleanup and new nonce protocol #e828bca
- rustix fs feature #e9805a8
- Added manage_go_signal command #ea70ec9
- Drop will no longer crash when the server goes down #eb3311a
- Made all errors type-based #ec2f414
- Added description on how the DownloadManager works #f029cbf
- Using more appropriate logging statements #f183a9d
- remove unnecessary compat code (#20) #f1c8bbf
- Manifests are now being parsed successfully #f28c880
- Removed tests/ #f29e989
- I think that downloads are working. Need to test and set decent file locations now #f388237
- Just debugging tauri's damn Sync command features #f60ca2b
- fixes and patches for merged changes #f6476bc
- Added manage_queue_signal #f64782e
- initial commit #f6cd7c3
- Update .gitlab-ci.yml #fc6bab9
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
## Release 0.1.0-beta
### Fixes

View file

@ -2,7 +2,13 @@ import { listen } from "@tauri-apps/api/event";
import type { DownloadableMetadata } from "~/types";
export type QueueState = {
queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
queue: Array<{
meta: DownloadableMetadata;
status: string;
progress: number | null;
current: number;
max: number;
}>;
status: string;
};
@ -25,4 +31,4 @@ listen("update_queue", (event) => {
listen("update_stats", (event) => {
const stats = useStatsState();
stats.value = event.payload as StatsState;
});
});

View file

@ -48,9 +48,7 @@ export function initialNavigation(state: Ref<AppState>) {
switch (state.value.status) {
case AppStatus.NotConfigured:
router.push({ path: "/setup" }).then(() => {
console.log("Pushed Setup");
});
router.push({ path: "/setup" });
break;
case AppStatus.SignedOut:
router.push("/auth");

View file

@ -1,7 +1,7 @@
<template>
<div class="flex flex-col bg-zinc-900 overflow-hidden">
<Header class="select-none" />
<div class="grow overflow-y-auto">
<div class="relative grow overflow-y-auto">
<slot />
</div>
</div>

View file

@ -1,7 +1,7 @@
<template>
<div class="flex flex-col bg-zinc-950 overflow-hidden">
<MiniHeader />
<div class="grow overflow-y-auto">
<div class="relative grow overflow-y-auto">
<slot />
</div>
</div>

View file

@ -1,7 +1,7 @@
{
"name": "drop-app",
"private": true,
"version": "0.1.1",
"version": "0.2.0-beta",
"type": "module",
"scripts": {
"build": "nuxt build",

View file

@ -26,7 +26,7 @@
import { XCircleIcon } from "@heroicons/vue/16/solid";
const route = useRoute();
const message = route.query.error ?? "An unknown error occurred.";
const message = route.query.error ?? "An unknown error occurred";
definePageMeta({
layout: "mini",

View file

@ -1,4 +1,7 @@
<template />
<script setup lang="ts">
definePageMeta({
layout: false
})
</script>

View file

@ -31,7 +31,7 @@
</ul>
</div>
<div class="grow overflow-y-auto">
<NuxtPage />
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
</div>
</div>
</template>
@ -40,7 +40,19 @@
import { invoke } from "@tauri-apps/api/core";
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
const rawGames: Array<Game> = await invoke("fetch_library");
let libraryDownloadError = false;
async function calculateGames(): Promise<Game[]> {
try {
return await invoke("fetch_library");
}
catch(e) {
libraryDownloadError = true;
return new Array();
}
}
const rawGames: Array<Game> = await calculateGames();
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
const icons = await Promise.all(
games.map(({ game, status }) => useObject(game.mIconId))

View file

@ -242,7 +242,7 @@
"
:loading="installLoading"
type="submit"
class="w-full sm:w-fit"
class="ml-2 w-full sm:w-fit"
>
Install
</LoadingButton>
@ -306,6 +306,7 @@ async function installFlow() {
versionOptions.value = await invoke("fetch_game_verion_options", {
gameId: game.value.id,
});
console.log(versionOptions.value);
installDirs.value = await invoke("fetch_download_dir_stats");
} catch (error) {
installError.value = (error as string).toString();
@ -319,19 +320,19 @@ const installDir = ref(0);
async function install() {
try {
if (!versionOptions.value)
throw new Error("Versions have not been loaded.");
throw new Error("Versions have not been loaded");
installLoading.value = true;
await invoke("download_game", {
gameId: game.value.id,
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
installDir: installDir.value,
});
installLoading.value = false;
installFlowOpen.value = false;
} catch (error) {
installError.value = (error as string).toString();
}
installLoading.value = false;
}
async function launch() {

View file

@ -1,3 +1,9 @@
<script setup lang="ts">
const props = defineProps<{ libraryDownloadError: boolean }>();
</script>
<template>
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
Library Failed to update
</div>
</template>

View file

@ -1,23 +1,37 @@
<template>
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
<div
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
>
<div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}</span>
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
>
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
<span v-if="stats.time > 0" class="text-sm"
>{{ formatTime(stats.time) }} left</span
>
</div>
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
<div v-for="bar in speedHistory" :style="{ height: `${bar / speedMax * 100}%` }"
class="w-[8px] bg-blue-600/40" />
<div
v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[8px] bg-blue-600/40"
/>
</div>
</div>
<draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<li v-if="games[element.meta.id]" :key="element.meta.id"
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
<li
v-if="games[element.meta.id]"
:key="element.meta.id"
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
>
<div class="w-full flex items-center max-w-md gap-x-4 relative">
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.meta.id].cover" alt="" />
<img
class="size-24 flex-none bg-zinc-800 object-cover rounded"
:src="games[element.meta.id].cover"
alt=""
/>
<div class="min-w-0 flex-auto">
<p class="text-xl font-semibold text-zinc-100">
<NuxtLink :href="`/library/${element.meta.id}`" class="">
@ -35,47 +49,68 @@
<p class="text-md text-zinc-500 uppercase font-display font-bold">
{{ element.status }}
</p>
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
<div
v-if="element.progress"
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
>
<div
class="h-2 bg-blue-600"
:style="{ width: `${element.progress * 100}%` }"
/>
</div>
<span
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}</span>
/
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
><ServerIcon class="size-5"
/></span>
</div>
<button @click="() => cancelGame(element.meta)" class="group">
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true" />
<XMarkIcon
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true"
/>
</button>
</div>
</li>
<p v-else>Loading...</p>
</template>
</draggable>
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
<div
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
v-if="queue.queue.length == 0"
>
No items in the queue
</div>
</div>
</template>
<script setup lang="ts">
import { XMarkIcon } from "@heroicons/vue/20/solid";
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
const windowWidth = ref(window.innerWidth);
window.addEventListener('resize', (event) => {
window.addEventListener("resize", (event) => {
windowWidth.value = window.innerWidth;
})
});
const queue = useQueueState();
const stats = useStatsState();
const speedHistory = useState<Array<number>>(() => []);
const speedHistoryMax = computed(() => windowWidth.value / 8);
const speedMax = computed(() => speedHistory.value.reduce((a, b) => a > b ? a : b) * 1.3);
const speedMax = computed(
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
);
const previousGameId = ref<string | undefined>();
const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
}> = ref({});
function resetHistoryGraph() {
speedHistory.value = [];
stats.value = { time: 0, speed: 0 };
@ -97,8 +132,7 @@ function checkReset(v: QueueState) {
return;
}
// If it's a different game now
if (currentGame != previousGameId.value
) {
if (currentGame != previousGameId.value) {
previousGameId.value = currentGame;
resetHistoryGraph();
return;
@ -115,10 +149,12 @@ watch(stats, (v) => {
speedHistory.value.splice(0, 1);
}
checkReset(queue.value);
})
});
function loadGamesForQueue(v: typeof queue.value) {
for (const { meta: { id } } of v.queue) {
for (const {
meta: { id },
} of v.queue) {
if (games.value[id]) return;
(async () => {
const gameData = await useGame(id);
@ -131,7 +167,7 @@ function loadGamesForQueue(v: typeof queue.value) {
loadGamesForQueue(queue.value);
async function onEnd(event: { oldIndex: number; newIndex: number }) {
await invoke("move_game_in_queue", {
await invoke("move_download_in_queue", {
oldIndex: event.oldIndex,
newIndex: event.newIndex,
});
@ -152,7 +188,7 @@ function formatKilobytes(bytes: number): string {
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
function formatTime(seconds: number): string {
@ -162,7 +198,7 @@ function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes}m ${Math.round(seconds % 60)}s`
return `${minutes}m ${Math.round(seconds % 60)}s`;
}
const hours = Math.floor(minutes / 60);

View file

@ -79,11 +79,14 @@
min="1"
max="32"
v-model="downloadThreads"
@keypress="validateNumberInput"
@paste="validatePaste"
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
<p class="mt-2 text-sm text-zinc-400">
The maximum number of concurrent download threads. Higher values may download faster but use more system resources. Default is 4.
The maximum number of concurrent download threads. Higher values may
download faster but use more system resources. Default is 4.
</p>
</div>
@ -91,9 +94,16 @@
<button
type="button"
@click="saveDownloadThreads"
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:bg-blue-600/50 disabled:cursor-not-allowed"
:disabled="saveState.loading"
:class="[
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
saveState.success
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
]"
>
Save Changes
{{ saveState.success ? 'Saved' : 'Save Changes' }}
</button>
</div>
</div>
@ -210,6 +220,7 @@ import {
} from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core";
import { type Settings } from "~/types";
const open = ref(false);
const currentDirectory = ref<string | undefined>(undefined);
@ -218,8 +229,13 @@ const createDirectoryLoading = ref(false);
const dirs = ref<Array<string>>([]);
const downloadThreads = ref(4);
const downloadThreadsChanged = computed(() => downloadThreads.value !== 4);
const settings = await invoke<Settings>("fetch_settings");
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
const saveState = reactive({
loading: false,
success: false
});
async function updateDirs() {
const newDirs = await invoke<Array<string>>("fetch_download_dir_stats");
@ -254,7 +270,7 @@ async function submitDirectory() {
try {
error.value = undefined;
if (!currentDirectory.value)
throw new Error("Please select a directory first.");
throw new Error("Please select a directory first");
createDirectoryLoading.value = true;
// Add directory
@ -278,7 +294,40 @@ async function deleteDirectory(index: number) {
}
async function saveDownloadThreads() {
//Would save download threads downloadThreads.value);
await invoke("update_settings", { newSettings: { maxDownloadThreads: downloadThreads.value } })
try {
saveState.loading = true;
await invoke("update_settings", {
newSettings: { maxDownloadThreads: downloadThreads.value },
});
// Show success state
saveState.success = true;
// Reset back to normal state after 2 seconds
setTimeout(() => {
saveState.success = false;
}, 2000);
} catch (error) {
console.error('Failed to save settings:', error);
} finally {
saveState.loading = false;
}
}
function validateNumberInput(event: KeyboardEvent) {
// Allow only numbers and basic control keys
if (!/^\d$/.test(event.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
event.preventDefault();
}
}
function validatePaste(event: ClipboardEvent) {
// Prevent paste if content contains non-numeric characters
const pastedData = event.clipboardData?.getData('text');
if (pastedData && !/^\d+$/.test(pastedData)) {
event.preventDefault();
}
}
</script>

View file

@ -1,7 +1,7 @@
export default defineNuxtPlugin((nuxtApp) => {
// Also possible
nuxtApp.hook("vue:error", (error, instance, info) => {
console.log(error);
console.error(error, info);
const router = useRouter();
router.replace(`/error`);
});

64
src-tauri/Cargo.lock generated
View file

@ -255,6 +255,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "atomic-instant-full"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -846,37 +852,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.91",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.91",
]
[[package]]
name = "derive_more"
version = "0.99.18"
@ -1031,18 +1006,17 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.1.0"
version = "0.2.0-beta-prerelease-1"
dependencies = [
"atomic-instant-full",
"boxcar",
"chrono",
"derive_builder",
"directories",
"hex",
"http",
"log",
"log4rs",
"md5",
"merge-struct",
"openssl",
"parking_lot 0.12.3",
"rayon",
@ -1052,7 +1026,6 @@ dependencies = [
"serde",
"serde-binary",
"serde_json",
"serde_merge",
"serde_with",
"shared_child",
"slice-deque",
@ -2455,16 +2428,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "merge-struct"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d82012d21e24135b839b6b9bebd622b7ff0cb40071498bc2d066d3a6d04dd4a"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -3913,17 +3876,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_merge"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606e91878516232ac3b16c12e063d4468d762f16d77e7aef14a1f2326c5f409b"
dependencies = [
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "serde_repr"
version = "0.1.19"

View file

@ -1,6 +1,6 @@
[package]
name = "drop-app"
version = "0.1.0"
version = "0.2.0-beta-prerelease-1"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2021"
@ -47,11 +47,9 @@ tauri-plugin-autostart = "2.0.0"
shared_child = "1.0.1"
serde_with = "3.12.0"
slice-deque = "0.3.0"
derive_builder = "0.20.2"
throttle_my_fn = "0.2.6"
parking_lot = "0.12.3"
merge-struct = "0.1.0"
serde_merge = "0.1.3"
atomic-instant-full = "0.1.0"
[dependencies.tauri]
version = "2.1.1"

View file

@ -1,4 +1,4 @@
use crate::DB;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
use log::debug;
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
@ -7,24 +7,24 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
let manager = app.autolaunch();
if enabled {
manager.enable().map_err(|e| e.to_string())?;
debug!("Enabled autostart");
debug!("enabled autostart");
} else {
manager.disable().map_err(|e| e.to_string())?;
debug!("Disabled autostart");
debug!("eisabled autostart");
}
// Store the state in DB
let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
let mut db_handle = borrow_db_mut_checked();
db_handle.settings.autostart = enabled;
drop(db_handle);
DB.save().map_err(|e| e.to_string())?;
save_db();
Ok(())
}
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
// First check DB state
let db_handle = DB.borrow_data().unwrap();
let db_handle = borrow_db_checked();
let db_state = db_handle.settings.autostart;
drop(db_handle);
@ -46,7 +46,7 @@ pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_
// New function to sync state on startup
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
let db_handle = borrow_db_checked();
let should_be_enabled = db_handle.settings.autostart;
drop(db_handle);
@ -56,10 +56,10 @@ pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
if current_state != should_be_enabled {
if should_be_enabled {
manager.enable().map_err(|e| e.to_string())?;
debug!("Synced autostart: enabled");
debug!("synced autostart: enabled");
} else {
manager.disable().map_err(|e| e.to_string())?;
debug!("Synced autostart: disabled");
debug!("synced autostart: disabled");
}
}

View file

@ -1,13 +1,23 @@
use log::{debug, info};
use log::{debug, error};
use tauri::AppHandle;
use crate::AppState;
#[tauri::command]
pub fn quit(app: tauri::AppHandle) {
cleanup_and_exit(&app);
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
cleanup_and_exit(&app, &state);
}
pub fn cleanup_and_exit(app: &AppHandle) {
debug!("Cleaning up and exiting application");
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
debug!("cleaning up and exiting application");
let download_manager = state.lock().unwrap().download_manager.clone();
match download_manager.ensure_terminated() {
Ok(res) => match res {
Ok(_) => debug!("download manager terminated correctly"),
Err(_) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{:?}", e),
}
app.exit(0);
}

View file

@ -1,9 +1,4 @@
use tauri::AppHandle;
use crate::{
autostart::{get_autostart_enabled_logic, toggle_autostart_logic},
AppState,
};
use crate::AppState;
#[tauri::command]
pub fn fetch_state(

View file

@ -6,74 +6,83 @@ use std::{
use serde_json::Value;
use crate::{database::settings::Settings, error::user_error::UserValue, DB};
use crate::{
database::{db::borrow_db_mut_checked, settings::Settings},
download_manager::internal_error::InternalError,
};
use super::{db::DATA_ROOT_DIR, debug::SystemData};
use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
debug::SystemData,
};
// Will, in future, return disk/remaining size
// Just returns the directories that have been set up
#[tauri::command]
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
let lock = DB.borrow_data().unwrap();
let lock = borrow_db_checked();
lock.applications.install_dirs.clone()
}
#[tauri::command]
pub fn delete_download_dir(index: usize) {
let mut lock = DB.borrow_data_mut().unwrap();
let mut lock = borrow_db_mut_checked();
lock.applications.install_dirs.remove(index);
drop(lock);
DB.save().unwrap();
save_db();
}
#[tauri::command]
pub fn add_download_dir(new_dir: PathBuf) -> UserValue<(), Error> {
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
// Check the new directory is all good
let new_dir_path = Path::new(&new_dir);
if new_dir_path.exists() {
let dir_contents = new_dir_path.read_dir()?;
if dir_contents.count() != 0 {
return UserValue::Err(Error::new(
return Err(Error::new(
ErrorKind::DirectoryNotEmpty,
"Selected directory cannot contain any existing files",
));
)
.into());
}
} else {
create_dir_all(new_dir_path)?;
}
// Add it to the dictionary
let mut lock = DB.borrow_data_mut().unwrap();
let mut lock = borrow_db_mut_checked();
if lock.applications.install_dirs.contains(&new_dir) {
return UserValue::Err(Error::new(
return Err(Error::new(
ErrorKind::AlreadyExists,
"Selected directory already exists in database",
));
)
.into());
}
lock.applications.install_dirs.push(new_dir);
drop(lock);
DB.save().unwrap();
save_db();
UserValue::Ok(())
Ok(())
}
#[tauri::command]
pub fn update_settings(new_settings: Value) {
println!("{}", new_settings);
let mut db_lock = DB.borrow_data_mut().unwrap();
let mut db_lock = borrow_db_mut_checked();
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
for (key, value) in new_settings.as_object().unwrap() {
current_settings[key] = value.clone();
}
println!("New settings unset: {}", &current_settings);
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings;
println!("New Settings: {:?}", db_lock.settings);
println!("new Settings: {:?}", db_lock.settings);
}
#[tauri::command]
pub fn fetch_settings() -> Settings {
borrow_db_checked().settings.clone()
}
#[tauri::command]
pub fn fetch_system_data() -> SystemData {
let db_handle = DB.borrow_data().unwrap();
let db_handle = borrow_db_checked();
SystemData::new(
db_handle.auth.as_ref().unwrap().client_id.clone(),
db_handle.base_url.clone(),

View file

@ -1,13 +1,14 @@
use std::{
collections::HashMap,
fs::{self, create_dir_all},
hash::Hash,
path::{Path, PathBuf},
sync::{LazyLock, Mutex, RwLockWriteGuard},
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
};
use chrono::Utc;
use directories::BaseDirs;
use log::{debug, info};
use log::{debug, error, info};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::serde_as;
@ -56,11 +57,23 @@ pub enum ApplicationTransientStatus {
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub version_index: usize,
pub game_id: String,
pub version_name: String,
pub launch_command: String,
pub setup_command: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
pub setup_command: String,
pub setup_args: Vec<String>,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
@ -91,10 +104,18 @@ impl Database {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
..Default::default()
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
..Default::default()
base_url: "".to_owned(),
auth: None,
settings: Settings {
autostart: false,
max_download_threads: 4,
},
}
}
}
@ -130,7 +151,7 @@ impl DatabaseImpls for DatabaseInterface {
let games_base_dir = data_root_dir.join("games");
let logs_root_dir = data_root_dir.join("logs");
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(games_base_dir.clone()).unwrap();
create_dir_all(logs_root_dir.clone()).unwrap();
@ -169,10 +190,10 @@ pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &Downloada
meta: DownloadableMetadata,
setter: F,
) {
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
setter(&mut db_handle, &meta);
drop(db_handle);
DB.save().unwrap();
save_db();
let status = GameStatusManager::fetch_state(&meta.id);
@ -187,16 +208,49 @@ fn handle_invalid_database(
let new_path = {
let time = Utc::now().timestamp();
let mut base = db_path.clone();
base.set_file_name(format!("drop.db.backup-{}", time.to_string()));
base.set_file_name(format!("drop.db.backup-{}", time));
base
};
info!("{:?}", new_path);
info!(
"old database stored at: {}",
new_path.to_string_lossy().to_string()
);
fs::rename(&db_path, &new_path).unwrap();
let db = Database::new(
games_base_dir.into_os_string().into_string().unwrap(),
Some(new_path.into()),
Some(new_path),
);
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
}
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
match DB.borrow_data() {
Ok(data) => data,
Err(e) => {
error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {}", e);
}
}
}
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
match DB.borrow_data_mut() {
Ok(data) => data,
Err(e) => {
error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {}", e);
}
}
}
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}

View file

@ -1,4 +1,3 @@
use crate::{DATA_ROOT_DIR, DB};
use serde::Serialize;
#[derive(Serialize)]

View file

@ -1,4 +1,4 @@
use std::sync::{mpsc::SendError, Arc, Mutex};
use std::sync::Mutex;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};

View file

@ -4,12 +4,12 @@ use std::{
fmt::Debug,
sync::{
mpsc::{SendError, Sender},
MutexGuard,
Mutex, MutexGuard,
},
thread::JoinHandle,
};
use log::info;
use log::{debug, info};
use serde::Serialize;
use crate::error::application_download_error::ApplicationDownloadError;
@ -84,7 +84,7 @@ pub enum DownloadStatus {
/// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager {
terminator: JoinHandle<Result<(), ()>>,
terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
download_queue: Queue,
progress: CurrentProgressObject,
command_sender: Sender<DownloadManagerSignal>,
@ -99,7 +99,7 @@ impl DownloadManager {
command_sender: Sender<DownloadManagerSignal>,
) -> Self {
Self {
terminator,
terminator: Mutex::new(Some(terminator)),
download_queue,
progress,
command_sender,
@ -110,7 +110,7 @@ impl DownloadManager {
&self,
download: DownloadAgent,
) -> Result<(), SendError<DownloadManagerSignal>> {
info!("Adding download id {:?}", download.metadata());
info!("creating download with meta {:?}", download.metadata());
self.command_sender
.send(DownloadManagerSignal::Queue(download))?;
self.command_sender.send(DownloadManagerSignal::Go)
@ -151,12 +151,14 @@ impl DownloadManager {
.unwrap();
}
info!("moving {} to {}", current_index, new_index);
debug!(
"moving download at index {} to index {}",
current_index, new_index
);
let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
info!("new queue: {:?}", queue);
drop(queue);
if needs_pause {
@ -165,6 +167,10 @@ impl DownloadManager {
self.command_sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
self.command_sender
.send(DownloadManagerSignal::Go)
.unwrap();
}
pub fn pause_downloads(&self) {
self.command_sender
@ -174,11 +180,12 @@ impl DownloadManager {
pub fn resume_downloads(&self) {
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
pub fn ensure_terminated(self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
self.command_sender
.send(DownloadManagerSignal::Finish)
.unwrap();
self.terminator.join()
let terminator = self.terminator.lock().unwrap().take();
terminator.unwrap().join()
}
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
self.command_sender

View file

@ -7,7 +7,7 @@ use std::{
thread::{spawn, JoinHandle},
};
use log::{debug, error, info};
use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter};
use crate::{
@ -179,13 +179,13 @@ impl DownloadManagerBuilder {
}
}
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
info!("Got signal Queue");
debug!("got signal Queue");
let meta = download_agent.metadata();
info!("Meta: {:?}", meta);
debug!("queue metadata: {:?}", meta);
if self.download_queue.exists(meta.clone()) {
info!("Download with same ID already exists");
warn!("download with same ID already exists");
return;
}
@ -199,9 +199,9 @@ impl DownloadManagerBuilder {
}
fn manage_go_signal(&mut self) {
info!("Got signal Go");
debug!("got signal Go");
if self.download_agent_registry.is_empty() {
info!(
debug!(
"Download agent registry: {:?}",
self.download_agent_registry.len()
);
@ -209,19 +209,21 @@ impl DownloadManagerBuilder {
}
if self.current_download_agent.is_some() {
info!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() {
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
}
info!("Current download queue: {:?}", self.download_queue.read());
debug!("current download queue: {:?}", self.download_queue.read());
// Should always be Some if the above two statements keep going
let agent_data = self.download_queue.read().front().unwrap().clone();
info!("Starting download for {:?}", agent_data);
info!("starting download for {:?}", agent_data);
let download_agent = self
.download_agent_registry
@ -265,7 +267,7 @@ impl DownloadManagerBuilder {
active_control_flag.set(DownloadThreadControlFlag::Go);
}
fn manage_stop_signal(&mut self) {
info!("Got signal Stop");
debug!("got signal Stop");
if let Some(active_control_flag) = self.active_control_flag.clone() {
self.set_status(DownloadManagerStatus::Paused);
@ -273,10 +275,9 @@ impl DownloadManagerBuilder {
}
}
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
info!("Got signal Completed");
debug!("got signal Completed");
if let Some(interface) = &self.current_download_agent {
if interface.metadata() == meta {
info!("Popping consumed data");
self.remove_and_cleanup_front_download(&meta);
}
}
@ -284,18 +285,17 @@ impl DownloadManagerBuilder {
self.sender.send(DownloadManagerSignal::Go).unwrap();
}
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
info!("Got signal Error");
let current_agent = self.current_download_agent.clone().unwrap();
current_agent.on_error(&self.app_handle, error.clone());
self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata());
debug!("got signal Error");
if let Some(current_agent) = self.current_download_agent.clone() {
current_agent.on_error(&self.app_handle, error.clone());
self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata());
}
self.set_status(DownloadManagerStatus::Error(error));
}
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
info!("Got signal Cancel");
debug!("got signal Cancel");
if let Some(current_download) = &self.current_download_agent {
if &current_download.metadata() == meta {
@ -306,32 +306,30 @@ impl DownloadManagerBuilder {
self.download_queue.pop_front();
self.cleanup_current_download();
info!("Current donwload queue: {:?}", self.download_queue.read());
debug!("current download queue: {:?}", self.download_queue.read());
}
// TODO: Collapse these two into a single if statement somehow
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
info!("Object exists in registry");
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta);
info!(
"Removed {:?} from queue {:?}",
debug!(
"removed {:?} from queue {:?}",
removed.map(|x| x.metadata()),
self.download_queue.read()
);
}
}
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
info!("Object exists in registry");
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta);
info!(
"Removed {:?} from queue {:?}",
debug!(
"removed {:?} from queue {:?}",
removed.map(|x| x.metadata()),
self.download_queue.read()
);
@ -354,6 +352,8 @@ impl DownloadManagerBuilder {
meta: DownloadableMetadata::clone(key),
status: val.status(),
progress: val.progress().get_progress(),
current: val.progress().sum(),
max: val.progress().get_max(),
}
})
.collect();

View file

@ -0,0 +1,27 @@
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)
}
}

View file

@ -4,5 +4,7 @@ pub mod download_manager_builder;
pub mod download_thread_control_flag;
pub mod downloadable;
pub mod downloadable_metadata;
pub mod internal_error;
pub mod progress_object;
pub mod queue;
pub mod rolling_progress_updates;

View file

@ -2,15 +2,17 @@ use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::Sender,
Arc, Mutex, RwLock,
Arc, Mutex,
},
time::{Duration, Instant},
};
use log::info;
use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle;
use super::download_manager::DownloadManagerSignal;
use super::{
download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
};
#[derive(Clone)]
pub struct ProgressObject {
@ -18,10 +20,10 @@ pub struct ProgressObject {
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
start: Arc<Mutex<Instant>>,
sender: Sender<DownloadManagerSignal>,
points_towards_update: Arc<AtomicUsize>,
points_to_push_update: Arc<AtomicUsize>,
last_update: Arc<RwLock<Instant>>,
amount_last_update: Arc<AtomicUsize>,
//last_update: Arc<RwLock<Instant>>,
last_update_time: Arc<AtomicInstant>,
bytes_last_update: Arc<AtomicUsize>,
rolling: RollingProgressWindow<250>,
}
pub struct ProgressHandle {
@ -42,67 +44,32 @@ impl ProgressHandle {
pub fn add(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
self.progress_object.check_push_update(amount);
calculate_update(&self.progress_object);
}
pub fn skip(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
// Offset the bytes at last offset by this amount
self.progress_object
.bytes_last_update
.fetch_add(amount, Ordering::Relaxed);
// Dont' fire update
}
}
static PROGRESS_UPDATES: usize = 100;
impl ProgressObject {
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
// TODO: consolidate this calculation with the set_max function below
let points_to_push_update = max / PROGRESS_UPDATES;
Self {
max: Arc::new(Mutex::new(max)),
progress_instances: Arc::new(arr),
start: Arc::new(Mutex::new(Instant::now())),
sender,
points_towards_update: Arc::new(AtomicUsize::new(0)),
points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)),
last_update: Arc::new(RwLock::new(Instant::now())),
amount_last_update: Arc::new(AtomicUsize::new(0)),
}
}
pub fn check_push_update(&self, amount_added: usize) {
let current_amount = self
.points_towards_update
.fetch_add(amount_added, Ordering::Relaxed);
let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
if current_amount >= to_update {
self.points_towards_update
.fetch_sub(to_update, Ordering::Relaxed);
update_queue(&self);
}
let last_update = self.last_update.read().unwrap();
let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
if last_update_difference > 1000 {
// push update
drop(last_update);
let mut last_update = self.last_update.write().unwrap();
*last_update = Instant::now();
drop(last_update);
let current_amount = self.sum();
let max = self.get_max();
let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
self.amount_last_update
.store(current_amount, Ordering::Relaxed);
let amount_since_last_update = current_amount - amount_at_last_update;
let kilobytes_per_second =
amount_since_last_update / (last_update_difference as usize).max(1);
let remaining = max - current_amount; // bytes
let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
update_ui(&self, kilobytes_per_second, time_remaining);
last_update_time: Arc::new(AtomicInstant::now()),
bytes_last_update: Arc::new(AtomicUsize::new(0)),
rolling: RollingProgressWindow::new(),
}
}
@ -122,9 +89,6 @@ impl ProgressObject {
}
pub fn set_max(&self, new_max: usize) {
*self.max.lock().unwrap() = new_max;
self.points_to_push_update
.store(new_max / PROGRESS_UPDATES, Ordering::Relaxed);
info!("points to push update: {}", new_max / PROGRESS_UPDATES);
}
pub fn set_size(&self, length: usize) {
*self.progress_instances.lock().unwrap() =
@ -136,9 +100,43 @@ impl ProgressObject {
pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
self.progress_instances.lock().unwrap()[index].clone()
}
fn update_window(&self, kilobytes_per_second: usize) {
self.rolling.update(kilobytes_per_second);
}
}
#[throttle(1, Duration::from_millis(20))]
pub fn calculate_update(progress: &ProgressObject) {
let last_update_time = progress
.last_update_time
.swap(Instant::now(), Ordering::SeqCst);
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
let current_bytes_downloaded = progress.sum();
let max = progress.get_max();
let bytes_at_last_update = progress
.bytes_last_update
.swap(current_bytes_downloaded, Ordering::Relaxed);
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
let bytes_remaining = max - current_bytes_downloaded; // bytes
progress.update_window(kilobytes_per_second);
push_update(progress, bytes_remaining);
}
#[throttle(1, Duration::from_millis(500))]
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
let average_speed = progress.rolling.get_average();
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
update_ui(progress, average_speed, time_remaining);
update_queue(progress);
}
#[throttle(50, Duration::from_secs(1))]
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
progress_object
.sender
@ -149,11 +147,9 @@ fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time
.unwrap();
}
#[throttle(50, Duration::from_secs(1))]
fn update_queue(progress: &ProgressObject) {
progress
.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}

View file

@ -32,7 +32,7 @@ impl Queue {
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
self.edit().pop_front()
}
pub fn empty(&self) -> bool {
pub fn is_empty(&self) -> bool {
self.inner.lock().unwrap().len() == 0
}
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
@ -52,10 +52,7 @@ impl Queue {
}
pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option<DownloadableMetadata> {
let mut queue = self.edit();
let front = match queue.front() {
Some(front) => front,
None => return None,
};
let front = queue.front()?;
if front == meta {
return queue.pop_front();
}

View file

@ -0,0 +1,33 @@
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Clone)]
pub struct RollingProgressWindow<const S: usize> {
window: Arc<[AtomicUsize; S]>,
current: Arc<AtomicUsize>,
}
impl<const S: usize> RollingProgressWindow<S> {
pub fn new() -> Self {
Self {
window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
current: Arc::new(AtomicUsize::new(0)),
}
}
pub fn update(&self, kilobytes_per_second: usize) {
let index = self.current.fetch_add(1, Ordering::SeqCst);
let current = &self.window[index % S];
current.store(kilobytes_per_second, Ordering::SeqCst);
}
pub fn get_average(&self) -> usize {
let current = self.current.load(Ordering::SeqCst);
self.window
.iter()
.enumerate()
.filter(|(i, _)| i < &current)
.map(|(_, x)| x.load(Ordering::Relaxed))
.sum::<usize>()
/ S
}
}

View file

@ -3,10 +3,12 @@ use std::{
io,
};
use serde_with::SerializeDisplay;
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
// TODO: Rename / separate from downloads
#[derive(Debug, Clone)]
#[derive(Debug, Clone, SerializeDisplay)]
pub enum ApplicationDownloadError {
Communication(RemoteAccessError),
Checksum,
@ -20,11 +22,11 @@ impl Display for ApplicationDownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
ApplicationDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
ApplicationDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
ApplicationDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
ApplicationDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
}
}
}

View file

@ -1,5 +1,8 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum LibraryError {
MetaNotFound(String),
}

View file

@ -4,4 +4,3 @@ pub mod library_error;
pub mod process_error;
pub mod remote_access_error;
pub mod setup_error;
pub mod user_error;

View file

@ -1,5 +1,8 @@
use std::{fmt::Display, io::Error};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum ProcessError {
SetupRequired,
NotInstalled,

View file

@ -5,11 +5,12 @@ use std::{
};
use http::StatusCode;
use serde_with::SerializeDisplay;
use url::ParseError;
use super::drop_server_error::DropServerError;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, SerializeDisplay)]
pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>),
ParsingError(ParseError),
@ -39,17 +40,17 @@ impl Display for RemoteAccessError {
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{}", parse_error)
}
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound => write!(f, "could not find game on server"),
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
f,
"Failed to download game manifest: {} {}",
"failed to download game manifest: {} {}",
status, response
),
RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
RemoteAccessError::Generic(message) => write!(f, "{}", message),
}
}

View file

@ -8,7 +8,7 @@ pub enum SetupError {
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Context => write!(f, "Failed to generate contexts for download"),
SetupError::Context => write!(f, "failed to generate contexts for download"),
}
}
}

View file

@ -1,66 +0,0 @@
use std::{
fmt::Display,
ops::{FromResidual, Try},
};
use serde::Serialize;
pub enum UserValue<T, D>
where
T: Serialize,
D: Display,
{
Ok(T),
Err(D),
}
impl<T: Serialize, D: Display> Serialize for UserValue<T, D> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
UserValue::Ok(data) => data.serialize(serializer),
UserValue::Err(err) => serializer.serialize_str(err.to_string().as_ref()),
}
}
}
impl<T: Serialize, D: Display> From<Result<T, D>> for UserValue<T, D> {
fn from(value: Result<T, D>) -> Self {
match value {
Ok(data) => UserValue::Ok(data),
Err(data) => UserValue::Err(data),
}
}
}
impl<T: Serialize, D: Display> Try for UserValue<T, D> {
type Output = T;
type Residual = D;
fn from_output(output: Self::Output) -> Self {
Self::Ok(output)
}
fn branch(self) -> std::ops::ControlFlow<Self::Residual, Self::Output> {
match self {
UserValue::Ok(data) => std::ops::ControlFlow::Continue(data),
UserValue::Err(e) => std::ops::ControlFlow::Break(e),
}
}
}
impl<T: Serialize, D: Display> FromResidual for UserValue<T, D> {
fn from_residual(residual: <Self as std::ops::Try>::Residual) -> Self {
UserValue::Err(residual)
}
}
impl<T: Serialize, D: Display, U> FromResidual<Result<U, D>> for UserValue<T, D> {
fn from_residual(residual: Result<U, D>) -> Self {
match residual {
Ok(_) => unreachable!(),
Err(e) => UserValue::Err(e),
}
}
}

View file

@ -3,32 +3,28 @@ use std::sync::Mutex;
use tauri::AppHandle;
use crate::{
error::{
library_error::LibraryError, remote_access_error::RemoteAccessError, user_error::UserValue,
},
games::library::{get_current_meta, uninstall_game_logic},
AppState,
database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState
};
use super::{
library::{
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
Game, GameVersionOption,
Game,
},
state::{GameStatusManager, GameStatusWithTransient},
};
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> UserValue<Vec<Game>, RemoteAccessError> {
fetch_library_logic(app).into()
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
fetch_library_logic(app)
}
#[tauri::command]
pub fn fetch_game(
game_id: String,
app: tauri::AppHandle,
) -> UserValue<FetchGameStruct, RemoteAccessError> {
fetch_game_logic(game_id, app).into()
) -> Result<FetchGameStruct, RemoteAccessError> {
fetch_game_logic(game_id, app)
}
#[tauri::command]
@ -37,21 +33,21 @@ pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
}
#[tauri::command]
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> UserValue<(), LibraryError> {
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
let meta = match get_current_meta(&game_id) {
Some(data) => data,
None => return UserValue::Err(LibraryError::MetaNotFound(game_id)),
None => return Err(LibraryError::MetaNotFound(game_id)),
};
println!("{:?}", meta);
uninstall_game_logic(meta, &app_handle);
UserValue::Ok(())
Ok(())
}
#[tauri::command]
pub fn fetch_game_verion_options(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<Vec<GameVersionOption>, RemoteAccessError> {
fetch_game_verion_options_logic(game_id, state).into()
) -> Result<Vec<GameVersion>, RemoteAccessError> {
fetch_game_verion_options_logic(game_id, state)
}

View file

@ -1,8 +1,10 @@
use std::sync::{mpsc::SendError, Arc, Mutex};
use std::sync::{Arc, Mutex};
use crate::{
download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable},
error::user_error::UserValue,
download_manager::{
download_manager::DownloadManagerSignal, downloadable::Downloadable,
internal_error::InternalError,
},
AppState,
};
@ -14,7 +16,7 @@ pub fn download_game(
game_version: String,
install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<(), SendError<DownloadManagerSignal>> {
) -> Result<(), InternalError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
@ -22,10 +24,9 @@ pub fn download_game(
install_dir,
sender,
)) as Box<dyn Downloadable + Send + Sync>);
state
Ok(state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)
.into()
.queue_download(game_download_agent)?)
}

View file

@ -1,6 +1,7 @@
use crate::auth::generate_authorization_header;
use crate::database::db::{
set_game_status, ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus,
borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
GameDownloadStatus,
};
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{
@ -13,11 +14,11 @@ use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
use crate::remote::requests::make_request;
use crate::DB;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
use slice_deque::SliceDeque;
use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::path::Path;
use std::sync::mpsc::Sender;
@ -55,7 +56,7 @@ impl GameDownloadAgent {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let db_lock = DB.borrow_data().unwrap();
let db_lock = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock);
@ -82,10 +83,8 @@ impl GameDownloadAgent {
// Blocking
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
self.ensure_manifest_exists()?;
info!("Ensured manifest exists");
self.ensure_contexts()?;
info!("Ensured contexts exists");
self.control_flag.set(DownloadThreadControlFlag::Go);
@ -94,11 +93,8 @@ impl GameDownloadAgent {
// Blocking
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
info!("Setting up download");
self.setup_download()?;
info!("Setting progress object params");
self.set_progress_object_params();
info!("Running");
let timer = Instant::now();
push_game_update(
app_handle,
@ -114,7 +110,7 @@ impl GameDownloadAgent {
.run()
.map_err(|_| ApplicationDownloadError::DownloadError);
info!(
debug!(
"{} took {}ms to download",
self.id,
timer.elapsed().as_millis()
@ -131,25 +127,17 @@ impl GameDownloadAgent {
}
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
let base_url = DB.fetch_base_url();
let manifest_url = base_url
.join(
format!(
"/api/v1/client/game/manifest?id={}&version={}",
self.id,
encode(&self.version)
)
.as_str(),
)
.unwrap();
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(manifest_url.to_string())
.header("Authorization", header)
.send()
.unwrap();
let response = make_request(
&client,
&["/api/v1/client/game/manifest"],
&[("id", &self.id), ("version", &self.version)],
|f| f.header("Authorization", header),
)
.map_err(|e| ApplicationDownloadError::Communication(e))?
.send()
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
return Err(ApplicationDownloadError::Communication(
@ -182,11 +170,8 @@ impl GameDownloadAgent {
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
debug!("Setting ProgressObject max to {}", chunk_count);
self.progress.set_max(chunk_count);
debug!("Setting ProgressObject size to {}", length);
self.progress.set_size(length);
debug!("Setting ProgressObject time to now");
self.progress.set_time_now();
}
@ -250,9 +235,9 @@ impl GameDownloadAgent {
// TODO: Change return value on Err
pub fn run(&self) -> Result<bool, ()> {
let max_download_threads = DB.borrow_data().unwrap().settings.max_download_threads;
let max_download_threads = borrow_db_checked().settings.max_download_threads;
info!(
debug!(
"downloading game: {} with {} threads",
self.id, max_download_threads
);
@ -264,8 +249,6 @@ impl GameDownloadAgent {
let completed_indexes = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_indexes.clone();
let base_url = DB.fetch_base_url();
let contexts = self.contexts.lock().unwrap();
pool.scope(|scope| {
let client = &reqwest::blocking::Client::new();
@ -275,15 +258,32 @@ impl GameDownloadAgent {
let progress = self.progress.get(index);
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it
if self.completed_contexts.lock().unwrap().contains(&index) {
progress_handle.add(context.length);
progress_handle.skip(context.length);
continue;
}
let sender = self.sender.clone();
let request = generate_request(&base_url, client, context);
let request = match make_request(
&client,
&["/api/v1/client/chunk"],
&[
("id", &context.game_id),
("version", &context.version),
("name", &context.file_name),
("chunk", &context.index.to_string()),
],
|r| r.header("Authorization", generate_authorization_header()),
) {
Ok(request) => request,
Err(e) => {
sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap();
continue;
},
};
scope.spawn(move |_| {
match download_game_chunk(context, &self.control_flag, progress_handle, request)
@ -298,7 +298,6 @@ impl GameDownloadAgent {
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
info!("Completed context id {}", index);
});
}
});
@ -307,28 +306,27 @@ impl GameDownloadAgent {
let completed_lock_len = {
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
for (item, _) in newly_completed.iter() {
completed_contexts_lock.push_front(item);
for (_, item) in newly_completed.iter() {
completed_contexts_lock.push_front(*item);
}
completed_contexts_lock.len()
};
info!("Got newly completed");
// If we're not out of contexts, we're not done, so we don't fire completed
if completed_lock_len != contexts.len() {
info!("da for {} exited without completing", self.id.clone());
info!(
"download agent for {} exited without completing ({}/{})",
self.id.clone(),
completed_lock_len,
contexts.len(),
);
self.stored_manifest
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
info!("Setting completed contexts");
self.stored_manifest.write();
info!("Wrote completed contexts");
return Ok(false);
}
info!("Sending completed signal");
// We've completed
self.sender
.send(DownloadManagerSignal::Completed(self.metadata()))
@ -338,27 +336,6 @@ impl GameDownloadAgent {
}
}
fn generate_request(
base_url: &url::Url,
client: reqwest::blocking::Client,
context: &DropDownloadContext,
) -> reqwest::blocking::RequestBuilder {
let chunk_url = base_url
.join(&format!(
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
// Encode the parts we don't trust
context.game_id,
encode(&context.version),
encode(&context.file_name),
context.index
))
.unwrap();
let header = generate_authorization_header();
client.get(chunk_url).header("Authorization", header)
}
impl Downloadable for GameDownloadAgent {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Downloading;
@ -407,6 +384,7 @@ impl Downloadable for GameDownloadAgent {
.unwrap();
}
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata();
*self.status.lock().unwrap() = DownloadStatus::Queued;

View file

@ -5,12 +5,12 @@ use crate::download_manager::progress_object::ProgressHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext;
use log::{error, warn};
use log::warn;
use md5::{Context, Digest};
use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions};
use std::io::Read;
use std::io::{ErrorKind, Read};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
@ -39,19 +39,17 @@ impl DropWriter<File> {
// Write automatically pushes to file and hasher
impl Write for DropWriter<File> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
/*
self.hasher.write_all(buf).map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!("Unable to write to hasher: {}", e),
)
})?;
*/
self.destination.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
// self.hasher.flush()?;
self.hasher.flush()?;
self.destination.flush()
}
}
@ -135,7 +133,6 @@ pub fn download_game_chunk(
if response.status() != 200 {
let err = response.json().unwrap();
warn!("{:?}", err);
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err),
));
@ -151,7 +148,7 @@ pub fn download_game_chunk(
let content_length = response.content_length();
if content_length.is_none() {
warn!("Recieved 0 length content from server");
warn!("recieved 0 length content from server");
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(response.json().unwrap()),
));
@ -179,16 +176,14 @@ pub fn download_game_chunk(
set_permissions(ctx.path.clone(), permissions).unwrap();
}
/*
let checksum = pipeline
.finish()
.map_err(|e| GameDownloadError::IoError(e))?;
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
let res = hex::encode(checksum.0);
if res != ctx.checksum {
return Err(GameDownloadError::Checksum);
return Err(ApplicationDownloadError::Checksum);
}
*/
Ok(true)
}

View file

@ -2,21 +2,20 @@ use std::fs::remove_dir_all;
use std::sync::Mutex;
use std::thread::spawn;
use log::{error, info, warn};
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use tauri::Emitter;
use tauri::{AppHandle, Manager};
use urlencoding::encode;
use crate::database::db::GameVersion;
use crate::database::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::process::process_manager::Platform;
use crate::remote::auth::generate_authorization_header;
use crate::{AppState, DB};
use crate::remote::requests::make_request;
use crate::AppState;
#[derive(serde::Serialize)]
pub struct FetchGameStruct {
@ -52,6 +51,8 @@ pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata,
pub status: DownloadStatus,
pub progress: f64,
pub current: usize,
pub max: usize,
}
#[derive(serde::Serialize, Clone)]
@ -65,31 +66,14 @@ pub struct StatsUpdateEvent {
pub time: usize,
}
// Game version with some fields missing and size information
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameVersionOption {
version_index: usize,
version_name: String,
platform: Platform,
setup_command: String,
launch_command: String,
delta: bool,
umu_id_override: Option<String>,
// total_size: usize,
}
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let library_url = base_url.join("/api/v1/client/user/library")?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(library_url.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
f.header("Authorization", header)
})?
.send()?;
if response.status() != 200 {
let err = response.json().unwrap();
@ -102,7 +86,7 @@ pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessErro
let state = app.state::<Mutex<AppState>>();
let mut handle = state.lock().unwrap();
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
for game in games.iter() {
handle.games.insert(game.id.clone(), game.clone());
@ -137,17 +121,11 @@ pub fn fetch_game_logic(
return Ok(data);
}
let base_url = DB.fetch_base_url();
let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
.send()?;
if response.status() == 404 {
return Err(RemoteAccessError::GameNotFound);
@ -161,7 +139,7 @@ pub fn fetch_game_logic(
let game: Game = response.json()?;
state_handle.games.insert(id.clone(), game.clone());
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
@ -183,18 +161,16 @@ pub fn fetch_game_logic(
pub fn fetch_game_verion_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let endpoint =
base_url.join(format!("/api/v1/client/game/versions?id={}", game_id).as_str())?;
let header = generate_authorization_header();
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(
&client,
&["/api/v1/client/game/versions"],
&[("id", &game_id)],
|r| r.header("Authorization", generate_authorization_header()),
)?
.send()?;
if response.status() != 200 {
let err = response.json().unwrap();
@ -202,11 +178,11 @@ pub fn fetch_game_verion_options_logic(
return Err(RemoteAccessError::InvalidResponse(err));
}
let data: Vec<GameVersionOption> = response.json()?;
let data: Vec<GameVersion> = response.json()?;
let state_lock = state.lock().unwrap();
let process_manager_lock = state_lock.process_manager.lock().unwrap();
let data: Vec<GameVersionOption> = data
let data: Vec<GameVersion> = data
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
.collect();
@ -217,8 +193,8 @@ pub fn fetch_game_verion_options_logic(
}
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
println!("Triggered uninstall for agent");
let mut db_handle = DB.borrow_data_mut().unwrap();
println!("triggered uninstall for agent");
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.transient_statuses
@ -233,7 +209,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
if previous_state.is_none() {
info!("uninstall job doesn't have previous state, failing silently");
warn!("uninstall job doesn't have previous state, failing silently");
return;
}
let previous_state = previous_state.unwrap();
@ -261,7 +237,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
error!("{}", e);
}
Ok(_) => {
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
@ -269,9 +245,9 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
DB.save().unwrap();
save_db();
info!("uninstalled game id {}", &meta.id);
debug!("uninstalled game id {}", &meta.id);
push_game_update(
&app_handle,
@ -284,8 +260,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
}
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
DB.borrow_data()
.unwrap()
borrow_db_checked()
.applications
.installed_game_version
.get(game_id)
@ -298,30 +273,27 @@ pub fn on_game_complete(
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
let base_url = DB.fetch_base_url();
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound);
}
let endpoint = base_url.join(
format!(
"/api/v1/client/game/version?id={}&version={}",
meta.id,
encode(meta.version.as_ref().unwrap())
)
.as_str(),
)?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(
&client,
&["/api/v1/client/metadata/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", header),
)?
.send()?;
let data: GameVersion = response.json()?;
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
@ -334,7 +306,7 @@ pub fn on_game_complete(
.insert(meta.id.clone(), meta.clone());
drop(handle);
DB.save().unwrap();
save_db();
let status = if data.setup_command.is_empty() {
GameDownloadStatus::Installed {
@ -348,13 +320,13 @@ pub fn on_game_complete(
}
};
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(db_handle);
DB.save().unwrap();
save_db();
app_handle
.emit(
&format!("update_game/{}", meta.id),

View file

@ -1,7 +1,4 @@
use crate::{
database::db::{ApplicationTransientStatus, GameDownloadStatus},
DB,
};
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
pub type GameStatusWithTransient = (
Option<GameDownloadStatus>,
@ -11,7 +8,7 @@ pub struct GameStatusManager {}
impl GameStatusManager {
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
let db_lock = DB.borrow_data().unwrap();
let db_lock = borrow_db_checked();
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
None => None,

View file

@ -1,5 +1,3 @@
#![feature(try_trait_v2)]
mod database;
mod games;
@ -16,10 +14,12 @@ use autostart::{get_autostart_enabled, toggle_autostart};
use cleanup::{cleanup_and_exit, quit};
use commands::fetch_state;
use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_system_data,
update_settings,
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
fetch_system_data, update_settings,
};
use database::db::{
borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
};
use database::db::{DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR};
use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
};
@ -44,8 +44,11 @@ use remote::auth::{self, generate_authorization_header, recieve_handshake};
use remote::commands::{
auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
};
use remote::requests::make_request;
use serde::{Deserialize, Serialize};
use std::env;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::{
collections::HashMap,
@ -92,15 +95,21 @@ pub struct AppState<'a> {
fn setup(handle: AppHandle) -> AppState<'static> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
.encoder(Box::new(PatternEncoder::new(
"{d} | {l} | {f}:{L} - {m}{n}",
)))
.append(false)
.build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log"))
.unwrap();
let console = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
.encoder(Box::new(PatternEncoder::new(
"{d} | {l} | {f}:{L} - {m}{n}",
)))
.build();
let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info"));
let config = Config::builder()
.appenders(vec![
Appender::builder().build("logfile", Box::new(logfile)),
@ -109,7 +118,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
.build(
Root::builder()
.appenders(vec!["logfile", "console"])
.build(LevelFilter::Info),
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
)
.unwrap();
@ -119,7 +128,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
debug!("Checking if database is set up");
debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
if !is_set_up {
return AppState {
@ -131,12 +140,12 @@ fn setup(handle: AppHandle) -> AppState<'static> {
};
}
debug!("Database is set up");
debug!("database is set up");
// TODO: Account for possible failure
let (app_status, user) = auth::setup().unwrap();
let (app_status, user) = auth::setup();
let db_handle = DB.borrow_data().unwrap();
let db_handle = borrow_db_checked();
let mut missing_games = Vec::new();
let statuses = db_handle.applications.game_statuses.clone();
drop(db_handle);
@ -166,7 +175,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
info!("detected games missing: {:?}", missing_games);
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
for game_id in missing_games {
db_handle
.applications
@ -177,11 +186,11 @@ fn setup(handle: AppHandle) -> AppState<'static> {
drop(db_handle);
info!("finished setup!");
debug!("finished setup!");
// Sync autostart state
if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
warn!("Failed to sync autostart state: {}", e);
warn!("failed to sync autostart state: {}", e);
}
AppState {
@ -218,6 +227,7 @@ pub fn run() {
fetch_system_data,
// User utils
update_settings,
fetch_settings,
// Auth
auth_initiate,
retry_connect,
@ -256,14 +266,14 @@ pub fn run() {
.setup(|app| {
let handle = app.handle().clone();
let state = setup(handle);
info!("initialized drop client");
debug!("initialized drop client");
app.manage(Mutex::new(state));
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
info!("registered all pre-defined deep links");
debug!("registered all pre-defined deep links");
}
let handle = app.handle().clone();
@ -283,7 +293,7 @@ pub fn run() {
.unwrap();
app.deep_link().on_open_url(move |event| {
info!("handling drop:// url");
debug!("handling drop:// url");
let binding = event.urls();
let url = binding.first().unwrap();
if url.host_str().unwrap() == "handshake" {
@ -313,18 +323,18 @@ pub fn run() {
app.webview_windows().get("main").unwrap().show().unwrap();
}
"quit" => {
cleanup_and_exit(app);
cleanup_and_exit(app, &app.state());
}
_ => {
println!("Menu event not handled: {:?}", event.id);
println!("menu event not handled: {:?}", event.id);
}
})
.build(app)
.expect("error while setting up tray menu");
{
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
if let Some(original) = db_handle.prev_database.take() {
warn!(
"Database corrupted. Original file at {}",
@ -347,23 +357,16 @@ pub fn run() {
Ok(())
})
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
let base_url = DB.fetch_base_url();
// Drop leading /
let object_id = &request.uri().path()[1..];
let object_url = base_url
.join("/api/v1/client/object/")
.unwrap()
.join(object_id)
.unwrap();
let header = generate_authorization_header();
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let response = client
.get(object_url.to_string())
.header("Authorization", header)
.send();
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
f.header("Authorization", header)
})
.unwrap()
.send();
if response.is_err() {
warn!(
"failed to fetch object with error: {}",

View file

@ -1,15 +1,12 @@
use std::sync::Mutex;
use crate::{
error::{process_error::ProcessError, user_error::UserValue},
AppState, DB,
};
use crate::{error::process_error::ProcessError, AppState};
#[tauri::command]
pub fn launch_game(
id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<(), ProcessError> {
) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
@ -21,24 +18,23 @@ pub fn launch_game(
match process_manager_lock.launch_process(id) {
Ok(_) => {}
Err(e) => return UserValue::Err(e),
Err(e) => return Err(e),
};
drop(process_manager_lock);
drop(state_lock);
UserValue::Ok(())
Ok(())
}
#[tauri::command]
pub fn kill_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> UserValue<(), ProcessError> {
) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock
.kill_game(game_id)
.map_err(ProcessError::IOError)
.into()
}

View file

@ -15,7 +15,9 @@ use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{
database::db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
database::db::{
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR
},
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
@ -64,12 +66,8 @@ impl ProcessManager<'_> {
}
}
// There's no easy way to distinguish between an executable name with
// spaces and it's arguments.
// I think if we just join the install_dir to whatever the user provides us, we'll be alright
// In future, we should have a separate field for executable name and it's arguments
fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec<String>) {
let root = raw_command;
fn process_command(&self, install_dir: &String, command: Vec<String>) -> (PathBuf, Vec<String>) {
let root = &command[0];
let install_dir = Path::new(install_dir);
let absolute_exe = install_dir.join(root);
@ -83,7 +81,7 @@ impl ProcessManager<'_> {
(absolute_exe, Vec::new())
}
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
return match self.processes.get(&game_id) {
match self.processes.get(&game_id) {
Some(child) => {
child.kill()?;
child.wait()?;
@ -93,7 +91,7 @@ impl ProcessManager<'_> {
io::ErrorKind::NotFound,
"Game ID not running",
)),
};
}
}
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
@ -106,7 +104,7 @@ impl ProcessManager<'_> {
self.processes.remove(&game_id);
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
let meta = db_handle
.applications
.installed_game_version
@ -166,9 +164,9 @@ impl ProcessManager<'_> {
{
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
Some(GameDownloadStatus::SetupRequired { .. }) => {
return Err(ProcessError::SetupRequired).into()
return Err(ProcessError::SetupRequired)
}
_ => return Err(ProcessError::NotInstalled).into(),
_ => return Err(ProcessError::NotInstalled),
};
let meta = DownloadableMetadata {
id: game_id.clone(),
@ -176,7 +174,7 @@ impl ProcessManager<'_> {
download_type: DownloadType::Game,
};
let mut db_lock = DB.borrow_data_mut().unwrap();
let mut db_lock = borrow_db_mut_checked();
debug!(
"Launching process {:?} with games {:?}",
&game_id, db_lock.applications.game_versions
@ -188,23 +186,18 @@ impl ProcessManager<'_> {
.get(&game_id)
.ok_or(ProcessError::NotInstalled)?;
let status_metadata: Option<(&String, &String)> = match game_status {
let (version_name, install_dir) = match game_status {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((&version_name, &install_dir)),
} => (version_name, install_dir),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded),
};
if status_metadata.is_none() {
return Err(ProcessError::NotDownloaded);
}
let (version_name, install_dir) = status_metadata.unwrap();
let game_version = db_lock
.applications
@ -214,19 +207,28 @@ impl ProcessManager<'_> {
.get(version_name)
.ok_or(ProcessError::InvalidVersion)?;
let raw_command: String = match game_status {
let mut command: Vec<String> = Vec::new();
match game_status {
GameDownloadStatus::Installed {
version_name: _,
install_dir: _,
} => game_version.launch_command.clone(),
} => {
command.extend([game_version.launch_command.clone()]);
command.extend(game_version.launch_args.clone());
},
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir: _,
} => game_version.setup_command.clone(),
} => {
command.extend([game_version.setup_command.clone()]);
command.extend(game_version.setup_args.clone());
},
_ => panic!("unreachable code"),
};
info!("Command: {:?}", &command);
let (command, args) = self.process_command(install_dir, raw_command);
let (command, args) = self.process_command(install_dir, command);
let target_current_dir = command.parent().unwrap().to_str().unwrap();
@ -274,8 +276,8 @@ impl ProcessManager<'_> {
let launch_process = game_launcher
.launch_process(
&meta,
command.to_str().unwrap().to_owned(),
args,
command.to_string_lossy().to_string(),
game_version,
target_current_dir,
log_file,
error_file,
@ -316,9 +318,6 @@ impl ProcessManager<'_> {
});
self.processes.insert(meta.id, wait_thread_handle);
info!("finished spawning process");
Ok(())
}
}
@ -333,8 +332,8 @@ pub trait ProcessHandler: Send + 'static {
fn launch_process(
&self,
meta: &DownloadableMetadata,
command: String,
args: Vec<String>,
launch_command: String,
game_version: &GameVersion,
current_dir: &str,
log_file: File,
error_file: File,
@ -346,17 +345,17 @@ impl ProcessHandler for NativeGameLauncher {
fn launch_process(
&self,
_meta: &DownloadableMetadata,
command: String,
args: Vec<String>,
launch_command: String,
game_version: &GameVersion,
current_dir: &str,
log_file: File,
error_file: File,
) -> Result<Child, Error> {
Command::new(command)
Command::new(PathBuf::from(launch_command))
.current_dir(current_dir)
.stdout(log_file)
.stderr(error_file)
.args(args)
.args(game_version.launch_args.clone())
.spawn()
}
}
@ -367,15 +366,21 @@ impl ProcessHandler for UMULauncher {
fn launch_process(
&self,
_meta: &DownloadableMetadata,
command: String,
args: Vec<String>,
launch_command: String,
game_version: &GameVersion,
_current_dir: &str,
_log_file: File,
_error_file: File,
) -> Result<Child, Error> {
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
.game_id(String::from("0"))
.launch_args(args)
println!("Game override: .{:?}.", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) ,
None => game_version.game_id.clone()
};
info!("Game ID: {}", game_id);
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, launch_command)
.game_id(game_id)
.launch_args(game_version.launch_args.clone())
.build()
.spawn()
}

View file

@ -1,18 +1,22 @@
use std::{env, sync::Mutex};
use chrono::Utc;
use log::{debug, error, info, warn};
use log::{debug, error, warn};
use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::db::{DatabaseAuth, DatabaseImpls},
database::db::{
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User, DB,
};
use super::requests::make_request;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct InitiateRequestBody {
@ -51,7 +55,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
pub fn generate_authorization_header() -> String {
let certs = {
let db = DB.borrow_data().unwrap();
let db = borrow_db_checked();
db.auth.clone().unwrap()
};
@ -65,16 +69,15 @@ pub fn generate_authorization_header() -> String {
pub fn fetch_user() -> Result<User, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let endpoint = base_url.join("/api/v1/client/user")?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
f.header("Authorization", header)
})?
.send()?;
if response.status() != 200 {
let err: DropServerError = response.json().unwrap();
let err: DropServerError = response.json()?;
warn!("{:?}", err);
if err.status_message == "Nonce expired" {
@ -97,7 +100,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
}
let base_url = {
let handle = DB.borrow_data().unwrap();
let handle = borrow_db_checked();
Url::parse(handle.base_url.as_str())?
};
@ -111,18 +114,18 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?;
debug!("Handshake responsded with {}", response.status().as_u16());
debug!("handshake responsded with {}", response.status().as_u16());
let response_struct: HandshakeResponse = response.json()?;
{
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
handle.auth = Some(DatabaseAuth {
private: response_struct.private,
cert: response_struct.certificate,
client_id: response_struct.id,
});
drop(handle);
DB.save().unwrap();
save_db();
}
{
@ -151,7 +154,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = DB.borrow_data().unwrap();
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
@ -166,7 +169,7 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
if response.status() != 200 {
let data: DropServerError = response.json()?;
error!("Could not start handshake: {}", data.status_message);
error!("could not start handshake: {}", data.status_message);
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
@ -180,15 +183,19 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
Ok(())
}
pub fn setup() -> Result<(AppStatus, Option<User>), RemoteAccessError> {
let data = DB.borrow_data().unwrap();
pub fn setup() -> (AppStatus, Option<User>) {
let data = borrow_db_checked();
let auth = data.auth.clone();
drop(data);
if auth.is_some() {
let user_result = fetch_user()?;
return Ok((AppStatus::SignedIn, Some(user_result)));
let user_result = match fetch_user() {
Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None),
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
};
return (AppStatus::SignedIn, Some(user_result));
}
Ok((AppStatus::SignedOut, None))
(AppStatus::SignedOut, None)
}

View file

@ -4,8 +4,9 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
error::{remote_access_error::RemoteAccessError, user_error::UserValue},
AppState, AppStatus, DB,
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError,
AppState, AppStatus,
};
use super::{
@ -17,31 +18,31 @@ use super::{
pub fn use_remote(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> UserValue<(), RemoteAccessError> {
UserValue::Ok(use_remote_logic(url, state)?)
) -> Result<(), RemoteAccessError> {
use_remote_logic(url, state)
}
#[tauri::command]
pub fn gen_drop_url(path: String) -> UserValue<String, RemoteAccessError> {
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
let base_url = {
let handle = DB.borrow_data().unwrap();
let handle = borrow_db_checked();
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
};
let url = base_url.join(&path).unwrap();
UserValue::Ok(url.to_string())
Ok(url.to_string())
}
#[tauri::command]
pub fn sign_out(app: AppHandle) {
// Clear auth from database
{
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
handle.auth = None;
drop(handle);
DB.save().unwrap();
save_db();
}
// Update app state
@ -57,19 +58,18 @@ pub fn sign_out(app: AppHandle) {
}
#[tauri::command]
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> UserValue<(), RemoteAccessError> {
let (app_status, user) = setup()?;
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
let (app_status, user) = setup();
let mut guard = state.lock().unwrap();
guard.status = app_status;
guard.user = user;
drop(guard);
UserValue::Ok(())
}
#[tauri::command]
pub fn auth_initiate() -> UserValue<(), RemoteAccessError> {
auth_initiate_logic().into()
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
auth_initiate_logic()
}
#[tauri::command]

View file

@ -1,3 +1,4 @@
pub mod auth;
pub mod commands;
pub mod remote;
pub mod requests;

View file

@ -1,15 +1,14 @@
use std::{
error::Error,
fmt::{Display, Formatter},
sync::{Arc, Mutex},
};
use std::sync::Mutex;
use http::StatusCode;
use log::{info, warn};
use log::{debug, warn};
use serde::Deserialize;
use url::{ParseError, Url};
use url::Url;
use crate::{error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB};
use crate::{
database::db::{borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError,
AppState, AppStatus,
};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@ -21,7 +20,7 @@ pub fn use_remote_logic(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<(), RemoteAccessError> {
info!("connecting to url {}", url);
debug!("connecting to url {}", url);
let base_url = Url::parse(&url)?;
// Test Drop url
@ -39,11 +38,11 @@ pub fn use_remote_logic(
app_state.status = AppStatus::SignedOut;
drop(app_state);
let mut db_state = DB.borrow_data_mut().unwrap();
let mut db_state = borrow_db_mut_checked();
db_state.base_url = base_url.to_string();
drop(db_state);
DB.save().unwrap();
save_db();
Ok(())
}

View file

@ -0,0 +1,23 @@
use reqwest::blocking::{Client, RequestBuilder};
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
client: &Client,
path_components: &[T],
query: &[(T, T)],
f: F,
) -> Result<RequestBuilder, RemoteAccessError> {
let mut base_url = DB.fetch_base_url();
for endpoint in path_components {
base_url = base_url.join(endpoint.as_ref())?;
}
{
let mut queries = base_url.query_pairs_mut();
for (param, val) in query {
queries.append_pair(param.as_ref(), val.as_ref());
}
}
let response = client.get(base_url);
Ok(f(response))
}

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.1.0",
"version": "0.2.0-beta",
"identifier": "dev.drop.app",
"build": {
"beforeDevCommand": "yarn dev --port 1432",
@ -23,7 +23,7 @@
},
"bundle": {
"active": true,
"targets": ["nsis", "deb", "rpm", "dmg"],
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
"windows": {
"nsis": {
"installMode": "both"

View file

@ -71,4 +71,9 @@ export type DownloadableMetadata = {
id: string,
version: string,
downloadType: DownloadableType
}
export type Settings = {
autostart: boolean,
maxDownloadThreads: number,
}