use crate::bridge::library::mirror::options::{LibraryTransport, MirrorCounts, MirrorOptions};
use indicatrix_net::library::{AngleSettingWire, DesignRecord, LibraryRequest, LibraryResponse};
use indicatrix_vault::{
db::sqlite::Database,
model::{
angle::AngleSetting, detail::FacetingDiagramDetail, entry::FacetingDiagramEntry,
file::AttachedFile, mirror::MirrorState,
},
};
use std::sync::{Arc, Mutex, PoisonError};
use tracing::warn;
pub(super) enum SyncedDesign {
Saved,
SkippedLocalConflict,
}
pub(super) fn sync_one_design(
db: &Arc<Mutex<Database>>,
transport: &impl LibraryTransport,
source_id: &str,
options: MirrorOptions,
summary: &indicatrix_net::library::DesignSummary,
is_new: bool,
counts: &mut MirrorCounts,
) -> Result<SyncedDesign, ()> {
let design = match transport.request(&LibraryRequest::FetchDesign {
entry_id: summary.entry_id,
}) {
Ok(LibraryResponse::Design(d)) => *d,
_ => return Err(()),
};
let attached_files = fetch_attachments(transport, &design, options, counts)?;
let entry = FacetingDiagramEntry {
title: design.title.clone(),
url: design.url.clone(),
design_id: design.design_id.clone().unwrap_or_default(),
};
let detail = build_detail(&design, attached_files, summary.concave_tiers);
let db = db.lock().unwrap_or_else(PoisonError::into_inner);
if is_new && is_unmirrored_local_row(&db, &design.url) {
warn!(
"Mirror sync: leaving local entry ({}) untouched -- it was never mirrored \
from this or any remote (local-row guard)",
design.url
);
return Ok(SyncedDesign::SkippedLocalConflict);
}
let Ok(id) = db.save_design(&entry, &detail, source_id) else {
return Err(());
};
if !is_new {
if let Err(e) = db.delete_preview_images(id) {
warn!(
"Mirror sync: failed to invalidate stale preview cache for entry #{id} \
({}): {e}",
design.url
);
}
if let Err(e) = db.delete_tilt_curves(id) {
warn!(
"Mirror sync: failed to invalidate stale tilt-curve cache for entry \
#{id} ({}): {e}",
design.url
);
}
if let Err(e) = db.delete_solid_extents(id) {
warn!(
"Mirror sync: failed to invalidate stale solid-extents cache for entry \
#{id} ({}): {e}",
design.url
);
}
}
let _ = db.upsert_mirror_state(&MirrorState {
url: design.url.clone(),
source_id: source_id.to_string(),
summary_version: summary.version,
design_version: design.version,
deleted_locally: false,
});
drop(db);
Ok(SyncedDesign::Saved)
}
fn fetch_attachments(
transport: &impl LibraryTransport,
design: &DesignRecord,
options: MirrorOptions,
counts: &mut MirrorCounts,
) -> Result<Vec<AttachedFile>, ()> {
let mut attached_files = Vec::with_capacity(design.attachments.len());
for meta in &design.attachments {
if meta.size > options.max_attachment_bytes {
counts.attachments_skipped_too_large += 1;
continue;
}
match transport.request(&LibraryRequest::FetchAttachment {
attachment_id: meta.id,
}) {
Ok(LibraryResponse::Attachment { name, content }) => {
counts.attachment_bytes_fetched += content.len() as u64;
counts.attachments_fetched += 1;
attached_files.push(AttachedFile {
name,
url: meta.url.clone(),
content,
});
}
Ok(LibraryResponse::NotFound) => {
}
_ => return Err(()),
}
}
Ok(attached_files)
}
fn build_detail(
design: &DesignRecord,
attached_files: Vec<AttachedFile>,
concave_tiers: u32,
) -> FacetingDiagramDetail {
let angle_settings_table: Vec<AngleSetting> =
design.angle_settings.iter().map(to_angle_setting).collect();
let concave_facets = angle_settings_table
.iter()
.filter(|a| a.tool.is_some())
.map(|a| a.index.split(',').filter(|i| !i.trim().is_empty()).count() as u32)
.sum();
FacetingDiagramDetail {
page_url: design.page_url.clone(),
diagram_image_name: design.diagram_image_name.clone(),
diagram_image_data: design.diagram_image_data.clone(),
angle_settings_table,
attached_files,
competition_diagram: design.competition_diagram.clone(),
lw_ratio: design.lw_ratio.clone(),
refractive_index: design.refractive_index.clone(),
index_gear: design.index_gear.clone(),
volume: design.volume.clone(),
facets_count: design.facets_count.clone(),
shape: design.shape.clone(),
designer_info: design.designer_info.clone(),
hw_ratio: design.hw_ratio.clone(),
tw_ratio: design.tw_ratio.clone(),
uw_ratio: design.uw_ratio.clone(),
pw_ratio: design.pw_ratio.clone(),
cw_ratio: design.cw_ratio.clone(),
symmetry_order: design.symmetry_order.clone(),
mirror_symmetry: design.mirror_symmetry,
designer: design.designer.clone(),
source_citation: design.source_citation.clone(),
pdf_file: design.pdf_file.clone(),
gem_file: design.gem_file.clone(),
shape_category: design.shape_category.clone(),
concave_tiers,
concave_facets,
}
}
fn is_unmirrored_local_row(db: &Database, url: &str) -> bool {
let Ok(Some(_)) = db.diagram_entry_id_for_url(url) else {
return false;
};
db.get_mirror_state(url).ok().flatten().is_none()
}
fn to_angle_setting(a: &AngleSettingWire) -> AngleSetting {
AngleSetting {
order_index: a.order_index,
facet: a.facet.clone(),
angle: a.angle.clone(),
index: a.index.clone(),
notes: a.notes.clone(),
tool: a
.tool_line
.as_deref()
.and_then(|line| line.split_whitespace().next())
.map(str::to_owned),
tool_line: a.tool_line.clone(),
}
}