chore: add vectum base

This commit is contained in:
Maarten van Heusden
2024-09-16 18:53:40 +02:00
parent 3df1b699c5
commit e7f4d6c0b3
36 changed files with 4990 additions and 1641 deletions

11
src-tauri/.gitignore vendored
View File

@@ -2,3 +2,14 @@
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
# DepotDownloader
depot/
downloads/
.DepotDownloader/
Games/
Depots/

2923
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,23 @@
[package]
name = "steamdepotdownloadergui"
version = "3.0.0"
description = "A Tauri App"
name = "vectum"
version = "3.0.0-alpha.1"
description = "Download older versions of Steam games with DepotDownloader"
authors = ["mmvanheusden"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] }
tauri-build = { version = "2.0.0-rc.11", features = [] }
[dependencies]
tauri = { version = "2.0.0-beta", features = [] }
tauri-plugin-shell = "2.0.0-beta"
serde_json = "1.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
tauri = { version = "2.0.0-rc.14", features = [] }
tauri-plugin-shell = "2.0.0-rc.3"
tauri-plugin-dialog = "2.0.0-rc.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
derive-getters = "0.5.0"
sha256 = "1.5.0"
reqwest = { version = "0.12.7" }
zip = "2.2.0"
async-process = "2.3.0"

View File

@@ -0,0 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"dialog:default",
"shell:default"
]
}

View File

@@ -1,18 +0,0 @@
{
"$schema": "./schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"shell:allow-open"
]
}

View File

@@ -1 +0,0 @@
{"default":{"identifier":"default","description":"Capability for the main window","context":"local","windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","shell:allow-open"],"platforms":["linux","macOS","windows","android","iOS"]}}

View File

@@ -1 +0,0 @@
{schema_str}

View File

@@ -1 +0,0 @@
{schema_str}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,77 @@
use std::fs::File;
use std::io::ErrorKind::AlreadyExists;
use std::{fs, io};
use std::{io::Write, path::Path};
use reqwest;
use sha256;
pub fn calc_checksum(path: &Path) -> io::Result<String> {
let bytes = fs::read(path)?;
let hash = sha256::digest(&bytes);
Ok(hash)
}
/// Downloads a file. The file will be saved to the [`filename`] provided.
pub async fn download_file(url: &str, filename: &Path) -> io::Result<()> {
if filename.exists() {
println!("DEBUG: Not downloading. File already exists.");
return Err(io::Error::from(AlreadyExists));
}
let mut file = File::create(filename)?;
let response = reqwest::get(url)
.await
.expect("Failed to contact internet.");
let content = response
.bytes()
.await
.expect("Failed to get response content.");
file.write_all(&content)?;
Ok(())
}
/// Unzips DepotDownloader zips
pub fn unzip(zip_file: &Path) -> io::Result<()> {
let file = File::open(zip_file)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = match file.enclosed_name() {
Some(path) => path,
None => continue
};
println!("Extracted {} from archive.", outpath.display());
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
// Copy over permissions from enclosed file to extracted file on UNIX systems.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
// If the mode `file.unix_mode()` is something (not None), copy it over to the extracted file.
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
}
// Set DepotDownloader executable.
if outpath.display().to_string() == "DepotDownloader" {
fs::set_permissions(&outpath, fs::Permissions::from_mode(0o755))?; // WTF is an octal?
}
}
}
Ok(())
}

View File

@@ -1,16 +1,125 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
use std::io;
use std::path::{Path};
use std::sync::OnceLock;
use std::time::Duration;
use tauri::{AppHandle, Emitter};
use crate::terminal::Terminal;
mod steam;
mod depotdownloader;
mod terminal;
static DEPOTDOWNLOADER_VERSION: &str = "2.7.1";
//TODO: arm
static DEPOTDOWNLOADER_LINUX_URL: &str = "https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_2.7.1/DepotDownloader-linux-x64.zip";
static DEPOTDOWNLOADER_WIN_URL: &str = "https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_2.7.1/DepotDownloader-windows-x64.zip";
// We create this variable now, and quickly populate it in preload_vectum(). Then we later access the data in start_download()
static TERMINAL: OnceLock<Vec<Terminal>> = OnceLock::new();
/// This function is called every time the app is reloaded/started. It quickly populates the [`TERMINAL`] variable with a working terminal.
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
async fn preload_vectum(app: AppHandle) {
// Only fill this variable once.
if TERMINAL.get().is_none() { TERMINAL.set(terminal::get_installed_terminals(true).await).expect("Failed to set available terminals") }
// Send the default terminal name to the frontend.
app.emit("default-terminal", Terminal::pretty_name(&TERMINAL.get().unwrap()[0])).unwrap();
}
#[tauri::command]
async fn start_download(steam_download: steam::SteamDownload) {
let default_terminal = TERMINAL.get().unwrap();
let working_dir = std::env::current_dir().unwrap();
let terminal_to_use = if steam_download.options().terminal().is_none() { default_terminal.first().unwrap() } else { &Terminal::from_index(&steam_download.options().terminal().unwrap()).unwrap() };
println!("\n\n---------------------HELLO FROM RUST!---------------------");
println!("We received these values from frontend:");
println!("\t- Username: {}", steam_download.username().as_ref().unwrap_or(&String::from("Not provided")));
println!("\t- Password: {}", steam_download.password().as_ref().unwrap_or(&String::from("Not provided")));
println!("\t- App ID: {}", steam_download.app_id());
println!("\t- Depot ID: {}", steam_download.depot_id());
println!("\t- Manifest ID: {}", steam_download.manifest_id());
println!("\t- Output Path: {}", steam_download.output_path());
println!("------------------------DEBUG INFORMATION-----------------");
println!("\t- Default terminal: {}", Terminal::pretty_name(&default_terminal[0]));
println!("\t- Terminal command: {:?}", terminal_to_use.create_command(&steam_download));
println!("\t- Working directory: {}", working_dir.display());
println!("----------------------------------------------------------");
terminal_to_use.create_command(&steam_download).spawn().ok();
}
/// Downloads the DepotDownloader zip file from the internet based on the OS.
#[tauri::command]
async fn download_depotdownloader() {
let url = if std::env::consts::OS == "windows" {
DEPOTDOWNLOADER_WIN_URL
} else {
DEPOTDOWNLOADER_LINUX_URL
};
// Where we store the DepotDownloader zip.
let zip_filename = format!("DepotDownloader-v{}-{}.zip", DEPOTDOWNLOADER_VERSION, std::env::consts::OS);
let depotdownloader_zip = Path::new(&zip_filename);
println!("Downloading DepotDownloader for {} to .{}{}", std::env::consts::OS, std::path::MAIN_SEPARATOR, depotdownloader_zip.display());
match depotdownloader::download_file(url, depotdownloader_zip).await {
Err(e) => {
if e.kind() == io::ErrorKind::AlreadyExists {
println!("DepotDownloader already exists. Skipping download.");
return;
}
println!("Failed to download DepotDownloader: {}", e);
return;
},
_ => {}
}
println!("Succesfully downloaded DepotDownloader from {}", url);
depotdownloader::unzip(depotdownloader_zip).unwrap();
println!("Succesfully extracted DepotDownloader zip.");
}
/// Checks internet connectivity using Google
#[tauri::command]
async fn internet_connection() -> bool {
let client = reqwest::Client::builder().timeout(Duration::from_secs(5)).build().unwrap();
client.get("https://connectivitycheck.android.com/generate_204").send().await.is_ok()
}
#[tauri::command]
async fn get_all_terminals(app: AppHandle) {
let terminals = terminal::get_installed_terminals(false).await;
terminals.iter().for_each(|terminal| {
println!("Terminal #{} ({}) is installed!", terminal.index().unwrap(), terminal.pretty_name());
// Sends: (terminal index aligned with dropdown; total terminals)
app.emit("working-terminal", (terminal.index(), Terminal::total())).unwrap();
});
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
println!();
tauri::Builder::default().plugin(tauri_plugin_dialog::init()).plugin(tauri_plugin_shell::init()).invoke_handler(tauri::generate_handler![
start_download,
download_depotdownloader,
internet_connection,
preload_vectum,
get_all_terminals
]).run(tauri::generate_context!()).expect("error while running tauri application");
}

41
src-tauri/src/steam.rs Normal file
View File

@@ -0,0 +1,41 @@
use std::path::PathBuf;
use derive_getters::Getters;
use serde::Deserialize;
/// Represents the data required to download a Steam depot.
#[derive(Deserialize, Debug, Getters)]
pub struct SteamDownload {
username: Option<String>,
password: Option<String>,
app_id: String,
depot_id: String,
manifest_id: String,
options: VectumOptions
}
#[derive(Debug, Deserialize, Getters)]
pub struct VectumOptions {
terminal: Option<u8>,
output_directory: Option<PathBuf>,
directory_name: Option<String>
}
impl SteamDownload {
/// If a username or password are not provided, the download is considered anonymous
pub fn is_anonymous(&self) -> bool {
self.username.is_none() || self.password.is_none()
}
/// The directory where the download should happen
pub fn output_path(&self) -> String {
let sep = std::path::MAIN_SEPARATOR.to_string();
match (&self.options.output_directory, &self.options.directory_name) {
(Some(output_dir), Some(dir_name)) => format!("{}{}{}", output_dir.display(), sep, dir_name),
(Some(output_dir), None) => format!("{}{}{}", output_dir.display(), sep, &self.manifest_id),
(None, Some(dir_name)) => format!(".{}{}", sep, dir_name),
(None, None) => format!(".{}{}", sep, &self.manifest_id)
}
}
}

394
src-tauri/src/terminal.rs Normal file
View File

@@ -0,0 +1,394 @@
use crate::steam::SteamDownload;
use async_process::Command;
use serde::Serialize;
/// Represents a terminal that can be used to run commands.
/// **Should be in sync with the terminal dropdown in the frontend.**
#[derive(Debug, Serialize, PartialEq)]
pub enum Terminal {
GNOMETerminal,
Alacritty,
Konsole,
GNOMEConsole,
Xfce4Terminal,
DeepinTerminal,
Terminator,
Terminology,
Kitty,
LXTerminal,
Tilix,
CoolRetroTerm,
XTerm,
CMD,
}
impl Terminal {
/// Iterates through each terminal
pub fn iter() -> impl Iterator<Item=Terminal> {
use self::Terminal::*;
vec![
GNOMETerminal, Alacritty, Konsole, GNOMEConsole, Xfce4Terminal, DeepinTerminal, Terminator, Terminology, Kitty, LXTerminal, Tilix, CoolRetroTerm, XTerm, CMD,
].into_iter()
}
/// Get terminal from index in order of the [`Terminal`] enum
pub fn from_index(index: &u8) -> Option<Terminal> {
Terminal::iter().nth(*index as usize)
}
/// Get the index of a terminal in the order of the [`Terminal`] enum
/// Returns `None` if the terminal is not found.
pub fn index(&self) -> Option<u8> {
Terminal::iter().position(|x| x == *self).map(|x| x as u8)
}
/// Get total number of terminals **possible** depending on the OS
pub fn total() -> u8 {
if cfg!(windows) {
return 1;
}
Terminal::iter().count() as u8 - 1 // -1 because cmd is not available on linux
}
/// Get the pretty name of a terminal
pub fn pretty_name(&self) -> &str {
match self {
Terminal::GNOMETerminal => "GNOME Terminal",
Terminal::GNOMEConsole => "GNOME Console",
Terminal::Konsole => "Konsole",
Terminal::Xfce4Terminal => "Xfce Terminal",
Terminal::Terminator => "Terminator",
Terminal::Terminology => "Terminology",
Terminal::XTerm => "XTerm",
Terminal::Kitty => "Kitty",
Terminal::LXTerminal => "LXTerminal",
Terminal::Tilix => "Tilix",
Terminal::DeepinTerminal => "Deepin Terminal",
Terminal::CoolRetroTerm => "cool-retro-term",
Terminal::Alacritty => "Alacritty",
Terminal::CMD => "CMD",
}
}
//region Probing a terminal
/// Checks if a [`Terminal`] is installed.
/// **See:** [`get_installed_terminals`]
pub async fn installed(&self) -> bool {
match self {
Terminal::CMD => {
let mut cmd = Command::new("cmd");
cmd.arg("/?").output().await.is_ok()
}
Terminal::GNOMETerminal => {
let mut cmd = Command::new("gnome-terminal");
cmd.arg("--version").output().await.is_ok()
}
Terminal::GNOMEConsole => {
let mut cmd = Command::new("kgx");
cmd.arg("--version").output().await.is_ok()
}
Terminal::Konsole => {
let mut cmd = Command::new("konsole");
cmd.arg("--version").output().await.is_ok()
}
Terminal::Xfce4Terminal => {
let mut cmd = Command::new("xfce4-terminal");
cmd.arg("--version").output().await.is_ok()
}
Terminal::Terminator => {
let mut cmd = Command::new("terminator");
cmd.arg("--version").output().await.is_ok()
}
Terminal::Terminology => {
let mut cmd = Command::new("terminology");
cmd.arg("--version").output().await.is_ok()
}
Terminal::XTerm => {
let mut cmd = Command::new("xterm");
cmd.arg("-v").output().await.is_ok()
}
Terminal::Kitty => {
let mut cmd = Command::new("kitty");
cmd.arg("--version").output().await.is_ok()
}
Terminal::LXTerminal => {
let mut cmd = Command::new("lxterminal");
cmd.arg("--version").output().await.is_ok()
}
Terminal::Tilix => {
let mut cmd = Command::new("tilix");
cmd.arg("--version").output().await.is_ok()
}
Terminal::DeepinTerminal => {
let mut cmd = Command::new("deepin-terminal");
cmd.arg("--version").output().await.is_ok()
}
Terminal::CoolRetroTerm => {
let mut cmd = Command::new("cool-retro-term");
cmd.arg("--version").output().await.is_ok()
}
Terminal::Alacritty => {
let mut cmd = Command::new("alacritty");
cmd.arg("--version").output().await.is_ok()
}
}
}
//endregion
//region Running a command in the terminal
/**
Returns a [`Command`] that, when executed should open the terminal and run the command.
## Commands
`{command}` = `{command};echo Command finished with code $?;sleep infinity`
| Terminal | Command to open terminal |
|----------------|---------------------------------------------------------------------------|
| CMD | `start cmd.exe /k {command}` |
| GNOMETerminal | `gnome-terminal -- /usr/bin/env sh -c {command}` |
| GNOMEConsole | `kgx -e /usr/bin/env sh -c {command}` |
| Konsole | `konsole -e /usr/bin/env sh -c {command}` |
| Xfce4Terminal | `xfce4-terminal -x /usr/bin/env sh -c {command}` |
| Terminator | `terminator -T "Downloading depot..." -e {command}` |
| Terminology | `terminology -e /usr/bin/env sh -c {command}` |
| XTerm | `xterm -hold -T "Downloading depot..." -e /usr/bin/env sh -c {command}` |
| Kitty | `kitty /usr/bin/env sh -c {command}` |
| LXTerminal | `lxterminal -e /usr/bin/env sh -c {command}` |
| Tilix | `tilix -e /usr/bin/env sh -c {command}` |
| DeepinTerminal | `deepin-terminal -e /usr/bin/env sh -c {command}` |
| CoolRetroTerm | `cool-retro-term -e /usr/bin/env sh -c {command}` |
| Alacritty | `alacritty -e /usr/bin/env sh -c {command}` |
*/
pub fn create_command(&self, steam_download: &SteamDownload) -> Command {
let command = create_depotdownloader_command(steam_download);
match self {
Terminal::CMD => {
let mut cmd = Command::new("cmd.exe");
cmd.args(&["/c", "start", "PowerShell.exe", "-NoExit", "-Command"]).args(command);
cmd
}
Terminal::GNOMETerminal => {
let mut cmd = Command::new("gnome-terminal");
cmd.args([
"--",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::GNOMEConsole => {
let mut cmd = Command::new("kgx");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c"
]).args(command);
cmd
}
Terminal::Konsole => {
let mut cmd = Command::new("konsole");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::Xfce4Terminal => {
let mut cmd = Command::new("xfce4-terminal");
cmd.args([
"-x",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::Terminator => {
let mut cmd = Command::new("terminator");
cmd.args([
"-T",
"Downloading depot...",
"-e",
]).args(command);
cmd
}
Terminal::Terminology => {
let mut cmd = Command::new("terminology");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::XTerm => {
let mut cmd = Command::new("xterm");
cmd.args([
"-hold",
"-T",
"Downloading depot...",
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::Kitty => {
let mut cmd = Command::new("kitty");
cmd.args([
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::LXTerminal => {
let mut cmd = Command::new("lxterminal");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::Tilix => {
let mut cmd = Command::new("tilix");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::DeepinTerminal => {
let mut cmd = Command::new("deepin-terminal");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::CoolRetroTerm => {
let mut cmd = Command::new("cool-retro-term");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
Terminal::Alacritty => {
let mut cmd = Command::new("alacritty");
cmd.args([
"-e",
"/usr/bin/env",
"sh",
"-c",
]).args(command);
cmd
}
}
}
//endregion
}
/**
Checks if terminals are installed by checking if they respond to commands.
## How it works
Probes a list of popular terminals and checks if they return an error when calling their `--version` or similar command line flag.
## Options
* `return_immediately`: [`bool`]: Return as soon as one terminal is found.
## Returns
A vector containing a list of terminals that should work.
## Commands
| Terminal | Command to check if installed |
|----------------|-------------------------------|
| CMD | `cmd /?` |
| GNOMETerminal | `gnome-terminal --version` |
| GNOMEConsole | `kgx --version` |
| Konsole | `konsole --version` |
| Xfce4Terminal | `xfce4-terminal --version` |
| Terminator | `terminator --version` |
| Terminology | `terminology --version` |
| XTerm | `xterm -v` |
| Kitty | `kitty --version` |
| LXTerminal | `lxterminal --version` |
| Tilix | `tilix --version` |
| DeepinTerminal | `deepin-terminal --version` |
| CoolRetroTerm | `cool-retro-term --version` |
| Alacritty | `alacritty --version` |
*/
pub async fn get_installed_terminals(return_immediately: bool) -> Vec<Terminal> {
#[cfg(windows)]
// For Windows, only CMD is available.
return vec!(Terminal::CMD);
let mut available_terminals: Vec<Terminal> = Vec::new();
for terminal in Terminal::iter() {
// Probe terminal. If it doesn't raise an error, it is probably installed.
if terminal.installed().await {
if return_immediately {
return vec![terminal];
}
available_terminals.push(terminal);
}
}
if available_terminals.is_empty() {
eprintln!("No terminals were detected. Try installing one.");
}
available_terminals
}
/// Creates the DepotDownloader command necessary to download the requested manifest.
fn create_depotdownloader_command(steam_download: &SteamDownload) -> Vec<String> {
let output_dir = if cfg!(windows) {
// In PowerShell, spaces can be escaped with a backtick.
steam_download.output_path().replace(" ", "` ")
} else {
// In bash, spaces can be escaped with a backslash.
steam_download.output_path().replace(" ", "\\ ")
};
if cfg!(not(windows)) {
if steam_download.is_anonymous() {
vec![format!(r#"./DepotDownloader -app {} -depot {} -manifest {} -dir {};echo Done!;sleep infinity"#, steam_download.app_id(), steam_download.depot_id(), steam_download.manifest_id(), output_dir)]
} else {
vec![format!(r#"./DepotDownloader -username {} -password {} -app {} -depot {} -manifest {} -dir {};echo Done!;sleep infinity"#, steam_download.username().clone().unwrap(), steam_download.password().clone().unwrap(), steam_download.app_id(), steam_download.depot_id(), steam_download.manifest_id(), output_dir)]
}
} else {
if steam_download.is_anonymous() {
vec![format!(r#".\DepotDownloader.exe -app {} -depot {} -manifest {} -dir {}"#, steam_download.app_id(), steam_download.depot_id(), steam_download.manifest_id(), output_dir)]
} else {
vec![format!(r#".\DepotDownloader.exe -username {} -password {} -app {} -depot {} -manifest {} -dir {}"#, steam_download.username().clone().unwrap(), steam_download.password().clone().unwrap(), steam_download.app_id(), steam_download.depot_id(), steam_download.manifest_id(), output_dir)]
}
}
}

View File

@@ -1,17 +1,21 @@
{
"productName": "SteamDepotDownloaderGUI",
"version": "3.0.0",
"identifier": "net.00pium.depotdownloader",
"version": "3.0.0-alpha.1",
"identifier": "net.oopium.depotdownloader",
"build": {
"frontendDist": "../src"
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "steamdepotdownloadergui",
"width": 800,
"height": 600
"title": "SteamDepotDownloaderGUI",
"width": 445,
"height": 650,
"resizable": false
}
],
"security": {