use std::io::Read;
use itertools::Itertools;
use crate::{
lang::TRANSLATOR,
path::StrictPath,
prelude::Error,
resource::{config::Config, manifest::Manifest},
scan::{compare_ranked_titles, layout::BackupLayout, BackupId, TitleFinder, TitleQuery},
};
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Input {
#[serde(default)]
pub config: ConfigOverride,
pub requests: Vec<Request>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ConfigOverride {
pub backup_path: Option<StrictPath>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(untagged, rename_all = "camelCase")]
pub enum Output {
Success {
responses: Vec<Response>,
},
Failure {
error: response::Error,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Request {
FindTitle(request::FindTitle),
CheckAppUpdate(request::CheckAppUpdate),
EditBackup(request::EditBackup),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Response {
Error(response::Error),
FindTitle(response::FindTitle),
CheckAppUpdate(response::CheckAppUpdate),
EditBackup(response::EditBackup),
}
pub mod request {
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct FindTitle {
pub multiple: bool,
pub backup: bool,
pub restore: bool,
pub steam_id: Option<u32>,
pub gog_id: Option<u64>,
pub lutris_id: Option<String>,
pub normalized: bool,
pub fuzzy: bool,
pub disabled: bool,
pub partial: bool,
pub names: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct CheckAppUpdate {}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct EditBackup {
pub game: String,
pub backup: Option<String>,
pub locked: Option<bool>,
pub comment: Option<String>,
}
}
pub mod response {
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct Error {
pub message: String,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct FindTitle {
pub titles: Vec<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct CheckAppUpdate {
pub update: Option<AppUpdate>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct AppUpdate {
pub version: String,
pub url: String,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(default, rename_all = "camelCase")]
pub struct EditBackup {}
}
fn parse_input(input: Option<String>) -> Result<Input, String> {
if let Some(input) = input {
let input = serde_json::from_str::<Input>(&input).map_err(|e| e.to_string())?;
Ok(input)
} else {
use std::io::IsTerminal;
let mut stdin = std::io::stdin();
if stdin.is_terminal() {
Ok(Input::default())
} else {
let mut bytes = vec![];
let _ = stdin.read_to_end(&mut bytes);
let raw = String::from_utf8_lossy(&bytes);
let input = serde_json::from_str::<Input>(&raw).map_err(|e| e.to_string())?;
Ok(input)
}
}
}
pub fn abort_error(error: Error) -> ! {
let output = Output::Failure {
error: response::Error {
message: TRANSLATOR.handle_error(&error),
},
};
println!("{}", serde_json::to_string_pretty(&output).unwrap());
std::process::exit(1);
}
pub fn abort_message(message: String) -> ! {
let output = Output::Failure {
error: response::Error { message },
};
println!("{}", serde_json::to_string_pretty(&output).unwrap());
std::process::exit(1);
}
pub fn process(input: Option<String>, config: &Config, manifest: &Manifest) -> Result<Output, String> {
let input = parse_input(input)?;
log::debug!("API input: {input:?}");
let mut responses = vec![];
let backup_path = input.config.backup_path.unwrap_or_else(|| config.restore.path.clone());
let layout = BackupLayout::new(backup_path);
let title_finder = TitleFinder::new(config, manifest, layout.restorable_game_set());
for request in input.requests {
match request {
Request::FindTitle(request::FindTitle {
multiple,
backup,
restore,
steam_id,
gog_id,
lutris_id,
normalized,
fuzzy,
disabled,
partial,
names,
}) => {
let titles = title_finder.find(TitleQuery {
multiple,
names,
steam_id,
gog_id,
lutris_id,
normalized,
fuzzy,
backup,
restore,
disabled,
partial,
});
let titles: Vec<_> = titles
.into_iter()
.sorted_by(compare_ranked_titles)
.map(|(name, _info)| name)
.collect();
responses.push(Response::FindTitle(response::FindTitle { titles }));
}
Request::CheckAppUpdate(request::CheckAppUpdate {}) => {
match crate::metadata::Release::fetch_sync(config.runtime.network_security) {
Ok(release) => {
let update = release.is_update().then(|| response::AppUpdate {
version: release.version.to_string(),
url: release.url,
});
responses.push(Response::CheckAppUpdate(response::CheckAppUpdate { update }));
}
Err(e) => {
responses.push(Response::Error(response::Error { message: e.to_string() }));
}
}
}
Request::EditBackup(request::EditBackup {
game,
backup,
locked,
comment,
}) => {
let backup = backup.map(BackupId::Named).unwrap_or(BackupId::Latest);
let Some(game) = title_finder.find_one_by_name(&game) else {
responses.push(Response::Error(response::Error {
message: TRANSLATOR.game_is_unrecognized(),
}));
continue;
};
let mut layout = layout.game_layout(&game);
if let Err(error) = layout.validate_id(&backup) {
responses.push(Response::Error(response::Error {
message: TRANSLATOR.handle_error(&error),
}));
continue;
}
if let Some(locked) = locked {
layout.set_backup_locked(&backup, locked);
}
if let Some(comment) = comment {
layout.set_backup_comment(&backup, &comment);
}
layout.save();
responses.push(Response::EditBackup(response::EditBackup {}));
}
}
}
Ok(Output::Success { responses })
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
pub fn deserialize_input() {
let serialized = r#"
{
"config": {
"backupPath": "/tmp"
},
"requests": [
{
"findTitle": {
"steamId": 10
}
}
]
}
"#
.trim();
let deserialized = serde_json::from_str::<Input>(serialized).unwrap();
let expected = Input {
config: ConfigOverride {
backup_path: Some(StrictPath::new("/tmp".to_string())),
},
requests: vec![Request::FindTitle(request::FindTitle {
steam_id: Some(10),
..Default::default()
})],
};
assert_eq!(expected, deserialized);
}
#[test]
pub fn serialize_output() {
let output = Output::Success {
responses: vec![Response::FindTitle(response::FindTitle {
titles: vec!["foo".to_string()],
})],
};
let serialized = serde_json::to_string_pretty(&output).unwrap();
let expected = r#"
{
"responses": [
{
"findTitle": {
"titles": [
"foo"
]
}
}
]
}
"#
.trim();
assert_eq!(expected, serialized);
}
}