use super::{
measure::{apply_measured_metadata, merge_reimport_metadata},
scan::{GEM_GCS_EXPLANATION, collect_import_candidates, find_native_sidecar},
};
use indicatrix_vault::{db::sqlite::Database, local};
use std::{
collections::HashSet,
path::Path,
sync::{Arc, Mutex},
};
use tracing::warn;
pub(super) fn catch_file_panic<T>(
f: impl FnOnce() -> T + std::panic::UnwindSafe,
) -> Result<T, String> {
std::panic::catch_unwind(f).map_err(|payload| {
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string())
})
}
pub(super) struct ImportOutcome {
pub(super) summary: String,
pub(super) imported_ids: Vec<i64>,
pub(super) had_failures: bool,
pub(super) had_collision: bool,
}
fn save_imported_design(
db: &Arc<Mutex<Database>>,
url: &str,
seen_before_in_batch: bool,
parsed: local::ImportedAsc,
file_name: &str,
) -> anyhow::Result<(i64, bool)> {
let db = db.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let is_collision = seen_before_in_batch || db.has_detail_for_entry_url(url).unwrap_or(false);
let local::ImportedAsc {
entry,
mut detail,
derived_from_entry_id,
} = parsed;
db.save_diagram_entry(&entry, local::LOCAL_SOURCE_ID)
.and_then(|id| {
if is_collision && let Ok(Some(existing)) = db.get_diagram_full(id) {
merge_reimport_metadata(&mut detail, &existing);
}
db.save_diagram_detail(&detail, id).map(|()| id)
})
.map(|id| {
if is_collision {
if let Err(e) = db.delete_preview_images(id) {
warn!(
"Import: failed to invalidate stale preview cache for entry #{id} \
('{file_name}'): {e}"
);
}
if let Err(e) = db.delete_tilt_curves(id) {
warn!(
"Import: failed to invalidate stale tilt-curve cache for entry #{id} \
('{file_name}'): {e}"
);
}
} else if let Some(source_id) = derived_from_entry_id {
match db.get_diagram_full(source_id) {
Ok(Some(_)) => {
if let Err(e) = db.set_derived_from_entry_id(id, Some(source_id)) {
warn!(
"Import: failed to record entry #{id} ('{file_name}') as \
derived from #{source_id}: {e}"
);
}
}
Ok(None) => {
}
Err(e) => warn!(
"Import: could not verify source entry #{source_id} for '{file_name}' \
before recording provenance: {e}"
),
}
}
(id, is_collision)
})
}
fn parse_one_import(
file_name: &str,
content: &str,
sidecar: Option<&(String, Vec<u8>)>,
) -> Result<local::ImportedAsc, String> {
let parse_result = catch_file_panic(std::panic::AssertUnwindSafe(|| {
local::import_asc(
file_name,
content,
sidecar.map(|(name, bytes)| (name.as_str(), bytes.as_slice())),
)
.map(|mut parsed| {
apply_measured_metadata(&mut parsed.detail);
parsed
})
}));
match parse_result {
Ok(Ok(parsed)) => Ok(parsed),
Ok(Err(e)) => Err(format!("{file_name} (parse error: {e})")),
Err(panic_msg) => {
warn!("Import panicked while processing '{file_name}': {panic_msg}");
Err(format!("{file_name} (internal error: {panic_msg})"))
}
}
}
pub(super) fn import_path(
db: &Arc<Mutex<Database>>,
path: &Path,
recurse: bool,
mut on_progress: impl FnMut(usize, usize),
) -> ImportOutcome {
let (candidates, gem_gcs_skipped) = match collect_import_candidates(path, recurse) {
Ok(c) => c,
Err(message) => {
return ImportOutcome {
summary: message,
imported_ids: Vec::new(),
had_failures: true,
had_collision: false,
};
}
};
let total = candidates.len();
let mut imported = 0usize;
let mut imported_ids: Vec<i64> = Vec::new();
let mut failed: Vec<String> = Vec::new();
let mut seen_in_batch: HashSet<String> = HashSet::new();
let mut replaced: Vec<String> = Vec::new();
for (i, file_path) in candidates.into_iter().enumerate() {
let file_name = file_path.file_name().map_or_else(
|| "unknown.asc".to_string(),
|n| n.to_string_lossy().into_owned(),
);
on_progress(i + 1, total);
let content = match std::fs::read_to_string(&file_path) {
Ok(c) => c,
Err(e) => {
failed.push(format!("{file_name} (read error: {e})"));
continue;
}
};
let url = format!("local://{file_name}");
let seen_before_in_batch = !seen_in_batch.insert(file_name.clone());
let sidecar = find_native_sidecar(&file_path);
let parsed = match parse_one_import(&file_name, &content, sidecar.as_ref()) {
Ok(parsed) => parsed,
Err(message) => {
failed.push(message);
continue;
}
};
let save_result = save_imported_design(db, &url, seen_before_in_batch, parsed, &file_name);
match save_result {
Ok((id, is_collision)) => {
imported += 1;
imported_ids.push(id);
if is_collision {
replaced.push(file_name);
}
}
Err(e) => failed.push(format!("{file_name} (save error: {e})")),
}
}
let mut summary = if failed.is_empty() {
format!("Imported {imported} .asc file(s).")
} else {
warn!(
"Import from {}: {} failed: {:?}",
path.display(),
failed.len(),
failed
);
format!(
"Imported {imported} .asc file(s); {} skipped ({}).",
failed.len(),
failed.join("; ")
)
};
if !replaced.is_empty() {
use std::fmt::Write as _;
let _ = write!(
summary,
" {} design(s) replaced an existing entry with the same file name ({}) -- \
filename-only matching, not a content comparison.",
replaced.len(),
replaced.join(", ")
);
}
if gem_gcs_skipped > 0 {
use std::fmt::Write as _;
let _ = write!(
summary,
" {gem_gcs_skipped} .gem/.gcs file(s) were not imported -- {GEM_GCS_EXPLANATION}"
);
}
ImportOutcome {
summary,
imported_ids,
had_failures: !failed.is_empty(),
had_collision: !replaced.is_empty(),
}
}