use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::time::{Instant, Duration};
use futures_util::StreamExt;
use md5::{Md5, Digest, digest::core_api::CoreWrapper};
use reqwest::{Client, Method, Response, header};
use std::path::{Path, PathBuf};
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use time::format_description::well_known::Rfc3339;
pub mod itch_api_types;
mod heuristics;
mod game_files_operations;
mod itch_manifest;
mod extract;
use crate::itch_api_types::*;
use crate::game_files_operations::*;
#[derive(Serialize, Clone, clap::ValueEnum, Eq, PartialEq, Hash)]
pub enum GamePlatform {
Linux,
Windows,
OSX,
Android,
Web,
Flash,
Java,
UnityWebPlayer,
}
impl std::fmt::Display for GamePlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", serde_json::to_string(&self).unwrap())
}
}
impl Upload {
pub fn to_game_platforms(&self) -> Vec<GamePlatform> {
let mut platforms: Vec<GamePlatform> = Vec::new();
match self.r#type {
UploadType::HTML => platforms.push(GamePlatform::Web),
UploadType::Flash => platforms.push(GamePlatform::Flash),
UploadType::Java => platforms.push(GamePlatform::Java),
UploadType::Unity => platforms.push(GamePlatform::UnityWebPlayer),
_ => (),
}
for t in self.traits.iter() {
match t {
UploadTrait::PLinux => platforms.push(GamePlatform::Linux),
UploadTrait::PWindows => platforms.push(GamePlatform::Windows),
UploadTrait::POSX => platforms.push(GamePlatform::OSX),
UploadTrait::PAndroid => platforms.push(GamePlatform::Android),
_ => ()
}
}
platforms
}
}
pub enum DownloadStatus {
Warning(String),
DownloadedCover {
game_cover_path: PathBuf
},
StartingDownload {
bytes_to_download: u64,
},
DownloadProgress {
downloaded_bytes: u64,
},
Extract,
}
pub enum LaunchMethod<'a> {
AlternativeExecutable(&'a Path),
ManifestAction(&'a str),
Heuristics(&'a GamePlatform, &'a Game),
}
#[derive(Serialize, Deserialize)]
pub struct InstalledUpload {
pub upload_id: u64,
pub game_folder: PathBuf,
pub upload: Option<Upload>,
pub game: Option<Game>,
}
impl InstalledUpload {
pub async fn add_missing_info(&mut self, client: &Client, api_key: &str, force_update: bool) -> Result<bool, String> {
let mut updated = false;
if self.upload.is_none() || force_update {
self.upload = Some(get_upload_info(client, api_key, self.upload_id).await?);
updated = true;
}
if self.game.is_none() || force_update {
self.game = Some(get_game_info(client, api_key, self.upload.as_ref().expect("The upload info has just been received. Why isn't it there?").game_id).await?);
updated = true;
}
Ok(updated)
}
}
impl std::fmt::Display for InstalledUpload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (u_name, u_created_at, u_updated_at, u_traits) = match self.upload.as_ref() {
None => ("", String::new(), String::new(), String::new()),
Some(u) => (
u.display_name.as_deref().unwrap_or(&u.filename),
u.created_at.format(&Rfc3339).unwrap_or_default(),
u.updated_at.format(&Rfc3339).unwrap_or_default(),
u.traits.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(", "),
)
};
let (g_id, g_name, g_description, g_url, g_created_at, g_published_at, a_id, a_name, a_url) = match self.game.as_ref() {
None => (String::new(), "", "", "", String::new(), String::new(), String::new(), "", ""),
Some(g) => (
g.id.to_string(),
g.title.as_str(),
g.short_text.as_deref().unwrap_or_default(),
g.url.as_str(),
g.created_at.format(&Rfc3339).unwrap_or_default(),
g.published_at.as_ref().and_then(|date| date.format(&Rfc3339).ok()).unwrap_or_default(),
g.user.id.to_string(),
g.user.display_name.as_deref().unwrap_or(&g.user.username),
g.user.url.as_str(),
)
};
write!(f, "\
Upload id: {}
Game folder: \"{}\"
Upload:
Name: {u_name}
Created at: {u_created_at}
Updated at: {u_updated_at}
Traits: {u_traits}
Game:
Id: {g_id}
Name: {g_name}
Description: {g_description}
URL: {g_url}
Created at: {g_created_at}
Published at: {g_published_at}
Author
Id: {a_id}
Name: {a_name}
URL: {a_url}",
self.upload_id,
self.game_folder.to_string_lossy(),
)
}
}
async fn itch_request(
client: &Client,
method: Method,
url: &ItchApiUrl<'_>,
api_key: &str,
options: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder
) -> Result<Response, String> {
let mut request: reqwest::RequestBuilder = client.request(method, url.to_string());
request = match url {
ItchApiUrl::V1(..) => request.header(header::AUTHORIZATION, format!("Bearer {api_key}")),
ItchApiUrl::V2(..) => request.header(header::AUTHORIZATION, api_key),
ItchApiUrl::Other(..) => request,
};
if let ItchApiUrl::V2(_) = url {
request = request.header(header::ACCEPT, "application/vnd.itch.v2");
}
request = options(request);
request.send().await
.map_err(|e| format!("Error while sending request: {e}"))
}
async fn itch_request_json<T>(
client: &Client,
method: Method,
url: &ItchApiUrl<'_>,
api_key: &str,
options: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder,
) -> Result<T, String> where
T: serde::de::DeserializeOwned,
{
let text = itch_request(client, method, url, api_key, options).await?
.text().await
.map_err(|e| format!("Error while reading response body: {e}"))?;
serde_json::from_str::<ApiResponse<T>>(&text)
.map_err(|e| format!("Error while parsing JSON body: {e}\n\n{}", text))?
.into_result()
}
async fn hash_readable_async(readable: impl tokio::io::AsyncRead + Unpin, hasher: &mut CoreWrapper<md5::Md5Core>) -> Result<(), String> {
let mut br = tokio::io::BufReader::new(readable);
loop {
let buffer = br.fill_buf().await
.map_err(|e| format!("Couldn't read file in order to hash it!\n{e}"))?;
if buffer.is_empty() {
break Ok(());
}
hasher.update(buffer);
let len = buffer.len();
br.consume(len);
}
}
async fn stream_response_into_file(
response: Response,
file: &mut tokio::fs::File,
mut md5_hash: Option<&mut CoreWrapper<md5::Md5Core>>,
progress_callback: impl Fn(u64),
callback_interval: Duration,
) -> Result<u64, String> {
let mut downloaded_bytes: u64 = 0;
let mut stream = response.bytes_stream();
let mut last_callback = Instant::now();
while let Some(chunk) = stream.next().await {
let chunk = chunk
.map_err(|e| format!("Error reading chunk: {e}"))?;
file.write_all(&chunk).await
.map_err(|e| format!("Error writing chunk to the file: {e}"))?;
if let Some(ref mut hasher) = md5_hash {
hasher.update(&chunk);
}
downloaded_bytes += chunk.len() as u64;
if last_callback.elapsed() > callback_interval {
last_callback = Instant::now();
progress_callback(downloaded_bytes);
}
}
progress_callback(downloaded_bytes);
Ok(downloaded_bytes)
}
async fn download_file(
client: &Client,
url: &ItchApiUrl<'_>,
api_key: &str,
file_path: &Path,
md5_hash: Option<&str>,
file_size_callback: impl Fn(u64),
progress_callback: impl Fn(u64),
callback_interval: Duration,
) -> Result<(), String> {
let mut md5_hash: Option<(CoreWrapper<md5::Md5Core>, &str)> = md5_hash.map(|s| (Md5::new(), s));
let partial_file_path: PathBuf = add_part_extension(file_path)?;
if tokio::fs::try_exists(file_path).await.map_err(|e| format!("Couldn't check is the file exists!: \"{}\"\n{e}", file_path.to_string_lossy()))? {
tokio::fs::rename(file_path, &partial_file_path).await
.map_err(|e| format!("Couldn't move the downloaded file:\n Source: \"{}\"\n Destination: \"{}\"\n{e}", file_path.to_string_lossy(), partial_file_path.to_string_lossy()))?;
}
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.read(true)
.open(&partial_file_path).await
.map_err(|e| format!("Couldn't open file: {}\n{e}", partial_file_path.to_string_lossy()))?;
let mut downloaded_bytes: u64 = file.metadata().await
.map_err(|e| format!("Couldn't get file metadata: {}\n{e}", partial_file_path.to_string_lossy()))?
.len();
let file_response: Option<Response> = 'r: {
let res = itch_request(client, Method::GET, url, api_key, |b| b).await?;
let download_size = res.content_length()
.ok_or_else(|| format!("Couldn't get the Content Length of the file to download!\n{res:?}"))?;
file_size_callback(download_size);
if downloaded_bytes == 0 {
break 'r Some(res);
}
else if downloaded_bytes == download_size {
break 'r None;
}
else if downloaded_bytes < download_size {
let part_res = itch_request(client, Method::GET, url, api_key,
|b| b.header(header::RANGE, format!("bytes={downloaded_bytes}-"))
).await?;
match part_res.status() {
reqwest::StatusCode::PARTIAL_CONTENT => break 'r Some(part_res),
reqwest::StatusCode::OK => (),
_ => return Err(format!("The HTTP server to download the file from didn't return HTTP code 200 nor 206, so exiting! It returned: {}\n{part_res:?}", part_res.status().as_u16())),
}
}
downloaded_bytes = 0;
file.set_len(0).await
.map_err(|e| format!("Couldn't remove old partially downloaded file: {}\n{e}", partial_file_path.to_string_lossy()))?;
Some(res)
};
if let Some((ref mut hasher, _)) = md5_hash && downloaded_bytes > 0 {
hash_readable_async(&mut file, hasher).await?;
}
if let Some(res) = file_response {
stream_response_into_file(res, &mut file, md5_hash.as_mut().map(|(h, _)| h), |b| progress_callback(downloaded_bytes + b), callback_interval).await?;
}
if let Some((hasher, hash)) = md5_hash {
let file_hash = format!("{:x}", hasher.finalize());
if !file_hash.eq_ignore_ascii_case(&hash) {
return Err(format!("File verification failed! The file hash and the hash provided by the server are different.\n
File hash: {file_hash}
Server hash: {hash}"
));
}
}
file.sync_all().await
.map_err(|e| e.to_string())?;
tokio::fs::rename(&partial_file_path, file_path).await
.map_err(|e| format!("Couldn't move the downloaded file:\n Source: \"{}\"\n Destination: \"{}\"\n{e}", partial_file_path.to_string_lossy(), file_path.to_string_lossy()))?;
Ok(())
}
async fn totp_verification(client: &Client, totp_token: &str, totp_code: u64) -> Result<LoginSuccess, String> {
itch_request_json::<LoginSuccess>(
client,
Method::POST,
&ItchApiUrl::V2("totp/verify"),
"",
|b| b.form(&[
("token", totp_token),
("code", &totp_code.to_string())
]),
).await
.map_err(|e| format!("An error occurred while attempting log in:\n{e}"))
}
pub async fn login(client: &Client, username: &str, password: &str, recaptcha_response: Option<&str>, totp_code: Option<u64>) -> Result<LoginSuccess, String> {
let mut params: Vec<(&'static str, &str)> = vec![
("username", username),
("password", password),
("force_recaptcha", "false"),
("source", "desktop"),
];
if let Some(rr) = recaptcha_response {
params.push(("recaptcha_response", rr));
}
let response = itch_request_json::<LoginResponse>(
client,
Method::POST,
&ItchApiUrl::V2("login"),
"",
|b| b.form(¶ms),
).await
.map_err(|e| format!("An error occurred while attempting log in:\n{e}"))?;
let ls = match response {
LoginResponse::CaptchaError(e) => {
return Err(format!(
r#"A reCAPTCHA verification is required to continue!
Go to "{}" and solve the reCAPTCHA.
To obtain the token, paste the following command on the developer console:
console.log(grecaptcha.getResponse())
Then run the login command again with the --recaptcha-response option."#,
e.recaptcha_url.as_str()
));
}
LoginResponse::TOTPError(e) => {
let Some(totp_code) = totp_code else {
return Err(format!(
r#"The accout has 2 step verification enabled via TOTP
Run the login command again with the --totp-code={{VERIFICATION_CODE}} option."#
));
};
totp_verification(client, e.token.as_str(), totp_code).await?
}
LoginResponse::Success(ls) => ls
};
Ok(ls)
}
pub async fn get_profile(client: &Client, api_key: &str) -> Result<User, String> {
itch_request_json::<ProfileResponse>(
client,
Method::GET,
&ItchApiUrl::V2("profile"),
api_key,
|b| b,
).await
.map(|res| res.user)
.map_err(|e| format!("An error occurred while attempting to get the profile info:\n{e}"))
}
pub async fn get_owned_keys(client: &Client, api_key: &str) -> Result<Vec<OwnedKey>, String> {
let mut keys: Vec<OwnedKey> = Vec::new();
let mut page: u64 = 1;
loop {
let mut response = itch_request_json::<OwnedKeysResponse>(
client,
Method::GET,
&ItchApiUrl::V2("profile/owned-keys"),
api_key,
|b| b.query(&[("page", page)]),
).await
.map_err(|e| format!("An error occurred while attempting to obtain the list of the user's game keys: {e}"))?;
let num_keys: u64 = response.owned_keys.len() as u64;
keys.append(&mut response.owned_keys);
if num_keys < response.per_page || num_keys == 0 {
break;
}
page += 1;
}
Ok(keys)
}
pub async fn get_game_info(client: &Client, api_key: &str, game_id: u64) -> Result<Game, String> {
itch_request_json::<GameInfoResponse>(
client,
Method::GET,
&ItchApiUrl::V2(&format!("games/{game_id}")),
api_key,
|b| b,
).await
.map(|res| res.game)
.map_err(|e| format!("An error occurred while attempting to obtain the game info:\n{e}"))
}
pub async fn get_game_uploads(client: &Client, api_key: &str, game_id: u64) -> Result<Vec<Upload>, String> {
itch_request_json::<GameUploadsResponse>(
client,
Method::GET,
&ItchApiUrl::V2(&format!("games/{game_id}/uploads")),
api_key,
|b| b,
).await
.map(|res| res.uploads)
.map_err(|e| format!("An error occurred while attempting to obtain the game uploads:\n{e}"))
}
pub fn get_game_platforms(uploads: &[Upload]) -> Vec<(u64, GamePlatform)> {
let mut platforms: Vec<(u64, GamePlatform)> = Vec::new();
for u in uploads {
for p in u.to_game_platforms() {
platforms.push((u.id, p));
}
}
platforms
}
pub async fn get_upload_info(client: &Client, api_key: &str, upload_id: u64) -> Result<Upload, String> {
itch_request_json::<UploadResponse>(
client,
Method::GET,
&ItchApiUrl::V2(&format!("uploads/{upload_id}")),
api_key,
|b| b,
).await
.map(|res| res.upload)
.map_err(|e| format!("An error occurred while attempting to obtain the upload information:\n{e}"))
}
pub async fn get_collections(client: &Client, api_key: &str) -> Result<Vec<Collection>, String> {
itch_request_json::<CollectionsResponse>(
client,
Method::GET,
&ItchApiUrl::V2("profile/collections"),
api_key,
|b| b,
).await
.map(|res| res.collections)
.map_err(|e| format!("An error occurred while attempting to obtain the list of the profile's collections:\n{e}"))
}
pub async fn get_collection_games(client: &Client, api_key: &str, collection_id: u64) -> Result<Vec<CollectionGameItem>, String> {
let mut games: Vec<CollectionGameItem> = Vec::new();
let mut page: u64 = 1;
loop {
let mut response = itch_request_json::<CollectionGamesResponse>(
client,
Method::GET,
&ItchApiUrl::V2(&format!("collections/{collection_id}/collection-games")),
api_key,
|b| b.query(&[("page", page)]),
).await
.map_err(|e| format!("An error occurred while attempting to obtain the list of the collection's games: {e}"))?;
let num_games: u64 = response.collection_games.len() as u64;
games.append(&mut response.collection_games);
if num_games < response.per_page || num_games == 0 {
break;
}
page += 1;
}
Ok(games)
}
pub async fn download_game_cover(client: &Client, api_key: &str, game_id: u64, folder: &Path, cover_filename: Option<&str>, force_download: bool) -> Result<Option<PathBuf>, String> {
let game_info = get_game_info(client, api_key, game_id).await?;
let Some(cover_url) = game_info.cover_url else {
return Ok(None);
};
tokio::fs::create_dir_all(folder).await
.map_err(|e| format!("Couldn't create the folder \"{}\": {e}", folder.to_string_lossy()))?;
let cover_filename = match cover_filename {
Some(f) => f,
None => COVER_IMAGE_DEFAULT_FILENAME,
};
let cover_path = folder.join(cover_filename);
if !force_download && cover_path.try_exists().map_err(|e| format!("Couldn't check if the game cover image exists: \"{}\"\n{e}", cover_path.to_string_lossy()))? {
return Ok(Some(cover_path));
}
download_file(
client,
&ItchApiUrl::Other(&cover_url),
"",
&cover_path,
None,
|_| (),
|_| (),
Duration::MAX,
).await?;
Ok(Some(cover_path))
}
pub async fn download_upload(
client: &Client,
api_key: &str,
upload_id: u64,
game_folder: Option<&Path>,
skip_hash_verification: bool,
upload_info: impl FnOnce(&Upload, &Game),
progress_callback: impl Fn(DownloadStatus),
callback_interval: Duration,
) -> Result<InstalledUpload, String> {
let upload: Upload = get_upload_info(client, api_key, upload_id).await?;
let game: Game = get_game_info(client, api_key, upload.game_id).await?;
upload_info(&upload, &game);
let game_folder = match game_folder {
Some(f) => f,
None => &get_game_folder(&game.title)?,
};
let upload_archive: PathBuf = get_upload_archive_path(game_folder, upload_id, &upload.filename);
tokio::fs::create_dir_all(&game_folder).await
.map_err(|e| format!("Couldn't create the folder \"{}\": {e}", game_folder.to_string_lossy()))?;
download_file(
client,
&ItchApiUrl::V2(&format!("uploads/{upload_id}/download")),
api_key,
&upload_archive,
upload.md5_hash.as_deref().filter(|_| !skip_hash_verification),
|bytes| progress_callback(DownloadStatus::StartingDownload { bytes_to_download: bytes } ),
|bytes| progress_callback(DownloadStatus::DownloadProgress { downloaded_bytes: bytes } ),
callback_interval,
).await?;
if skip_hash_verification {
progress_callback(DownloadStatus::Warning("Skipping hash verification! The file integrity won't be checked!".to_string()));
} else if upload.md5_hash.is_none() {
progress_callback(DownloadStatus::Warning("Missing md5 hash. Couldn't verify the file integrity!".to_string()));
}
progress_callback(DownloadStatus::Extract);
let upload_folder: PathBuf = get_upload_folder(game_folder, upload_id);
extract::extract(&upload_archive, &upload_folder).await
.map_err(|e| e.to_string())?;
Ok(InstalledUpload {
upload_id,
game_folder: game_folder.canonicalize()
.map_err(|e| format!("Error getting the canonical form of the game folder! Maybe it doesn't exist: {}\n{e}", game_folder.to_string_lossy()))?,
upload: Some(upload),
game: Some(game),
})
}
pub async fn import(client: &Client, api_key: &str, upload_id: u64, game_folder: &Path) -> Result<InstalledUpload, String> {
let upload: Upload = get_upload_info(client, api_key, upload_id).await?;
let game: Game = get_game_info(client, api_key, upload.game_id).await?;
Ok(InstalledUpload {
upload_id,
game_folder: game_folder.canonicalize()
.map_err(|e| format!("Error getting the canonical form of the game folder! Maybe it doesn't exist: {}\n{e}", game_folder.to_string_lossy()))?,
upload: Some(upload),
game: Some(game),
})
}
pub async fn remove_partial_download(client: &Client, api_key: &str, upload_id: u64, game_folder: Option<&Path>) -> Result<bool, String> {
let upload: Upload = get_upload_info(client, api_key, upload_id).await?;
let game: Game = get_game_info(client, api_key, upload.game_id).await?;
let game_folder = match game_folder {
Some(f) => f,
None => &get_game_folder(&game.title)?,
};
let to_be_removed_folders: &[PathBuf] = &[
add_part_extension(get_upload_folder(game_folder, upload_id))?,
];
let to_be_removed_files: &[PathBuf] = {
let upload_archive = get_upload_archive_path(game_folder, upload_id, &upload.filename);
&[
add_part_extension(&upload_archive)?,
upload_archive,
]
};
let mut was_something_deleted: bool = false;
for f in to_be_removed_files {
if f.try_exists().map_err(|e| format!("Couldn't check if the file exists: \"{}\"\n{e}", f.to_string_lossy()))? {
tokio::fs::remove_file(f).await
.map_err(|e| format!("Couldn't remove file: \"{}\"\n{e}", f.to_string_lossy()))?;
was_something_deleted = true;
}
}
for f in to_be_removed_folders {
if f.try_exists().map_err(|e| format!("Couldn't check if the folder exists: \"{}\"\n{e}", f.to_string_lossy()))? {
remove_folder_safely(f).await?;
was_something_deleted = true;
}
}
was_something_deleted |= remove_folder_if_empty(game_folder).await?;
Ok(was_something_deleted)
}
pub async fn remove(upload_id: u64, game_folder: &Path) -> Result<(), String> {
let upload_folder = get_upload_folder(game_folder, upload_id);
if is_folder_empty(&upload_folder)? {
return Ok(())
}
remove_folder_safely(upload_folder).await?;
remove_folder_if_empty(game_folder).await?;
Ok(())
}
pub async fn r#move(upload_id: u64, src_game_folder: &Path, dst_game_folder: &Path) -> Result<PathBuf, String> {
let src_upload_folder = get_upload_folder(src_game_folder, upload_id);
if !src_upload_folder.try_exists().map_err(|e| format!("Couldn't check if the upload folder exists: {e}"))? {
return Err(format!("The source game folder doesn't exsit!"));
}
let dst_upload_folder = get_upload_folder(dst_game_folder, upload_id);
if !is_folder_empty(&dst_upload_folder)? {
return Err(format!("The upload folder destination isn't empty!: \"{}\"", dst_upload_folder.to_string_lossy()));
}
move_folder(src_upload_folder.as_path(), dst_upload_folder.as_path()).await?;
remove_folder_if_empty(src_game_folder).await?;
dst_game_folder.canonicalize()
.map_err(|e| format!("Error getting the canonical form of the destination game folder! Maybe it doesn't exist: {}\n{e}", dst_game_folder.to_string_lossy()))
}
pub async fn get_upload_manifest(upload_id: u64, game_folder: &Path) -> Result<Option<itch_manifest::Manifest>, String> {
let upload_folder = get_upload_folder(game_folder, upload_id);
itch_manifest::read_manifest(&upload_folder)
}
pub async fn launch(
upload_id: u64,
game_folder: &Path,
launch_method: LaunchMethod<'_>,
wrapper: &[String],
game_arguments: &[String],
launch_start_callback: impl FnOnce(&Path, &str)
) -> Result<(), String> {
let upload_folder: PathBuf = get_upload_folder(game_folder, upload_id);
let (upload_executable, game_arguments): (&Path, Cow<[String]>) = match launch_method {
LaunchMethod::AlternativeExecutable(p) => (p, Cow::Borrowed(game_arguments)),
LaunchMethod::ManifestAction(a) => {
let ma = itch_manifest::launch_action(&upload_folder, Some(a))?
.ok_or_else(|| format!("The provided launch action doesn't exist in the manifest: {a}"))?;
(
&PathBuf::from(ma.path),
match game_arguments.is_empty(){
false => Cow::Borrowed(game_arguments),
true => Cow::Owned(ma.args.unwrap_or_default()),
},
)
}
LaunchMethod::Heuristics(gp, g) => {
let mao = itch_manifest::launch_action(&upload_folder, None)?;
match mao {
Some(ma) => (
&PathBuf::from(ma.path),
match game_arguments.is_empty(){
false => Cow::Borrowed(game_arguments),
true => Cow::Owned(ma.args.unwrap_or_default()),
},
),
None => (
&heuristics::get_game_executable(upload_folder.as_path(), gp, g).await?,
Cow::Borrowed(game_arguments),
)
}
}
};
let upload_executable = upload_executable.canonicalize()
.map_err(|e| format!("Error getting the canonical form of the upload executable path! Maybe it doesn't exist: {}\n{e}", upload_executable.to_string_lossy()))?;
make_executable(&upload_executable)?;
let mut game_process = {
let mut wrapper_iter = wrapper.iter();
match wrapper_iter.next() {
None => tokio::process::Command::new(&upload_executable),
Some(w) => {
let mut gp = tokio::process::Command::new(w);
gp.args(wrapper_iter.as_slice())
.arg(&upload_executable);
gp
}
}
};
game_process.current_dir(&upload_folder)
.args(game_arguments.as_ref());
launch_start_callback(upload_executable.as_path(), format!("{:?}", game_process).as_str());
let mut child = game_process.spawn()
.map_err(|e| {
let code = e.raw_os_error();
if code.is_some_and(|n| n == 8) {
format!("Couldn't spawn the child process because it is not an executable format for this OS\n\
Maybe a wrapper is missing or the selected game executable isn't the correct one!")
} else {
format!("Couldn't spawn the child process: {e}")
}
})?;
child.wait().await
.map_err(|e| format!("Error while awaiting for child exit!: {e}"))?;
Ok(())
}
pub fn get_web_game_url(upload_id: u64) -> String {
format!("https://html-classic.itch.zone/html/{upload_id}/index.html")
}