use std::{fs, path::Path};
use serde::Serialize;
use serde_json::{Value, json};
use toko_feed::{CollectionView, PokemonCardView, PokemonSetView};
use super::{
CliError, DEFAULT_MAX_PAGES, ListOptions, MAX_QUERY_LIMIT, Target, collect_keyset_pages,
fetch_card_page, fetch_collection_page, fetch_set_page,
};
const CANONICAL_BACKUP_FORMAT_VERSION: u32 = 1;
pub const DEFAULT_CANONICAL_BACKUP_PATH: &str = "data/canonical.json";
#[derive(Debug, Serialize)]
struct CanonicalBackup {
format_version: u32,
collections: Vec<CollectionView>,
pokemon_sets: Vec<PokemonSetView>,
pokemon_cards: Vec<PokemonCardView>,
}
pub fn execute(target: &Target, path: &Path) -> Result<Value, CliError> {
let options = ListOptions {
limit: MAX_QUERY_LIMIT,
after: None,
all: true,
max_pages: DEFAULT_MAX_PAGES,
};
let (collections, collection_pages) = collect_keyset_pages(&options, |after, limit| {
let page = fetch_collection_page(target, after, limit)?;
Ok((page.collections, page.next_after))
})?;
let (pokemon_sets, set_pages) = collect_keyset_pages(&options, |after, limit| {
let page = fetch_set_page(target, after, limit)?;
Ok((page.sets, page.next_after))
})?;
let (pokemon_cards, card_pages) = collect_keyset_pages(&options, |after, limit| {
let page = fetch_card_page(target, after, limit)?;
Ok((page.cards, page.next_after))
})?;
let backup = CanonicalBackup {
format_version: CANONICAL_BACKUP_FORMAT_VERSION,
collections,
pokemon_sets,
pokemon_cards,
};
write_backup(path, &backup)?;
Ok(json!({
"path": path,
"format_version": CANONICAL_BACKUP_FORMAT_VERSION,
"collections": backup.collections.len(),
"pokemon_sets": backup.pokemon_sets.len(),
"pokemon_cards": backup.pokemon_cards.len(),
"pages": collection_pages.saturating_add(set_pages).saturating_add(card_pages),
}))
}
fn write_backup(path: &Path, backup: &CanonicalBackup) -> Result<(), CliError> {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty());
if let Some(parent) = parent {
fs::create_dir_all(parent).map_err(|source| CliError::BackupIo {
action: "create parent for",
path: path.to_owned(),
source,
})?;
}
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
CliError::Usage("canonical backup path must name a UTF-8 JSON file".to_owned())
})?;
let temporary = path.with_file_name(format!(".{file_name}.{}.tmp", std::process::id()));
let mut bytes = serde_json::to_vec_pretty(backup)?;
bytes.push(b'\n');
fs::write(&temporary, bytes).map_err(|source| CliError::BackupIo {
action: "write",
path: temporary.clone(),
source,
})?;
if let Err(source) = fs::rename(&temporary, path) {
let _ = fs::remove_file(&temporary);
return Err(CliError::BackupIo {
action: "replace",
path: path.to_owned(),
source,
});
}
Ok(())
}