#![cfg(not(target_arch = "wasm32"))]
use super::kicad_import::{
part_from_symbol, FootprintChoice, KicadFiles, KicadLibrary, KicadPart, Folder,
};
use brep_ecad_core::Symbol;
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
pub const API: &str = "https://gitlab.com/api/v4";
pub const DEFAULT_TAG: &str = "9.0.9.1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Repo {
Symbols,
Footprints,
Packages3D,
}
impl Repo {
pub fn project(self) -> &'static str {
match self {
Repo::Symbols => "kicad/libraries/kicad-symbols",
Repo::Footprints => "kicad/libraries/kicad-footprints",
Repo::Packages3D => "kicad/libraries/kicad-packages3D",
}
}
pub fn folder(self) -> &'static str {
match self {
Repo::Symbols => "symbols",
Repo::Footprints => "footprints",
Repo::Packages3D => "3dmodels",
}
}
}
pub fn encode(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(byte as char),
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
pub fn raw_url(repo: Repo, tag: &str, path: &str) -> String {
format!(
"{API}/projects/{}/repository/files/{}/raw?ref={}",
encode(repo.project()),
encode(path),
encode(tag)
)
}
pub fn tree_url(repo: Repo, tag: &str, path: &str, page: u32) -> String {
format!(
"{API}/projects/{}/repository/tree?ref={}&path={}&per_page=100&page={}",
encode(repo.project()),
encode(tag),
encode(path),
page.max(1)
)
}
pub fn parse_tree(body: &str) -> Result<Vec<String>, String> {
let value: Value = serde_json::from_str(body).map_err(|error| format!("bad listing: {error}"))?;
if let Some(message) = value.get("message").and_then(Value::as_str) {
return Err(message.to_owned());
}
let entries = value.as_array().ok_or("the listing is not an array")?;
Ok(entries
.iter()
.filter_map(|entry| entry.get("name").and_then(Value::as_str))
.map(str::to_owned)
.collect())
}
pub fn symbol_libraries(names: &[String]) -> Vec<String> {
let mut found: Vec<String> = names
.iter()
.filter_map(|name| name.strip_suffix(".kicad_sym"))
.map(str::to_owned)
.collect();
found.sort();
found
}
pub type Fetcher = Box<dyn Fn(&str) -> Result<Vec<u8>, String> + Send + Sync>;
pub const FETCH_ATTEMPTS: u32 = 4;
pub fn retry_delay(status: u16, retry_after: Option<&str>, attempt: u32) -> Option<std::time::Duration> {
if status != 429 && !(500..600).contains(&status) {
return None;
}
if attempt + 1 >= FETCH_ATTEMPTS {
return None;
}
let seconds = retry_after
.and_then(|value| value.trim().parse::<u64>().ok())
.unwrap_or(1u64 << attempt.min(6));
Some(std::time::Duration::from_secs(seconds.clamp(1, 60)))
}
pub fn looks_missing(error: &str) -> bool {
error.contains("HTTP 404") || error.contains("404 Tree Not Found") || error.contains("404 File Not Found")
}
pub fn http_fetcher() -> Fetcher {
Box::new(|url: &str| http_get(url, &|delay| std::thread::sleep(delay)))
}
pub fn http_get(url: &str, wait: &dyn Fn(std::time::Duration)) -> Result<Vec<u8>, String> {
let mut attempt = 0;
loop {
let response = ehttp::fetch_blocking(&ehttp::Request::get(url)).map_err(|error| error.to_string())?;
if response.ok {
return Ok(response.bytes);
}
match retry_delay(response.status, response.headers.get("retry-after"), attempt) {
Some(delay) => {
wait(delay);
attempt += 1;
}
None => return Err(format!("HTTP {} {}", response.status, response.status_text)),
}
}
}
pub struct RemoteFiles {
root: PathBuf,
tag: String,
fetch: Fetcher,
listings: Mutex<HashMap<String, Vec<String>>>,
scope: Option<String>,
fetched: Mutex<Vec<String>>,
bytes: Mutex<u64>,
problems: Mutex<Vec<String>>,
}
impl RemoteFiles {
pub fn new(root: impl Into<PathBuf>, tag: impl Into<String>, fetch: Fetcher) -> Self {
Self {
root: root.into(),
tag: tag.into(),
fetch,
listings: Mutex::new(HashMap::new()),
scope: None,
fetched: Mutex::new(Vec::new()),
bytes: Mutex::new(0),
problems: Mutex::new(Vec::new()),
}
}
pub fn scoped_to(mut self, library: impl Into<String>) -> Self {
self.scope = Some(library.into());
self
}
pub fn library(&self) -> KicadLibrary {
let folder = |repo: Repo| Folder {
path: self.path_of(repo).to_string_lossy().into_owned(),
from: format!("KiCad's {} library, {}", repo.project().rsplit('/').next().unwrap_or(""), self.tag),
};
KicadLibrary { footprints: folder(Repo::Footprints), models: folder(Repo::Packages3D) }
}
pub fn path_of(&self, repo: Repo) -> PathBuf {
self.root.join(repo.folder())
}
pub fn fetched(&self) -> Vec<String> {
self.fetched.lock().map(|list| list.clone()).unwrap_or_default()
}
pub fn take_fetched(&self) -> Vec<String> {
self.fetched.lock().map(|mut list| std::mem::take(&mut *list)).unwrap_or_default()
}
pub fn fetched_bytes(&self) -> u64 {
self.bytes.lock().map(|count| *count).unwrap_or(0)
}
pub fn take_problems(&self) -> Vec<String> {
self.problems.lock().map(|mut list| std::mem::take(&mut *list)).unwrap_or_default()
}
fn note_problem(&self, what: &str, error: &str) {
if looks_missing(error) {
return;
}
if let Ok(mut list) = self.problems.lock() {
if list.len() < 64 {
list.push(format!("{what}: {error}"));
}
}
}
pub fn prepare(&self) -> Result<(), String> {
for repo in [Repo::Symbols, Repo::Footprints, Repo::Packages3D] {
std::fs::create_dir_all(self.path_of(repo))
.map_err(|error| format!("the KiCad cache folder {} was not made: {error}", self.path_of(repo).display()))?;
}
Ok(())
}
pub fn fetch_file(&self, repo: Repo, path: &str) -> Result<Vec<u8>, String> {
let local = self.path_of(repo).join(path);
if let Ok(bytes) = std::fs::read(&local) {
return Ok(bytes);
}
let bytes = (self.fetch)(&raw_url(repo, &self.tag, path)).map_err(|error| {
self.note_problem(&format!("{}/{path}", repo.folder()), &error);
format!("{}: {error}", repo.project())
})?;
if let Ok(mut count) = self.bytes.lock() {
*count += bytes.len() as u64;
}
if let Some(dir) = local.parent() {
std::fs::create_dir_all(dir).map_err(|error| format!("{}: {error}", dir.display()))?;
}
std::fs::write(&local, &bytes).map_err(|error| format!("{}: {error}", local.display()))?;
if let Ok(mut list) = self.fetched.lock() {
list.push(format!("{}/{path}", repo.folder()));
}
Ok(bytes)
}
fn tree(&self, repo: Repo, path: &str) -> Vec<String> {
let key = format!("{}/{path}", repo.folder());
if let Some(cached) = self.listings.lock().ok().and_then(|map| map.get(&key).cloned()) {
return cached;
}
let mut names = Vec::new();
let mut whole = false;
for page in 1..=64 {
let body = match (self.fetch)(&tree_url(repo, &self.tag, path, page)) {
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
Err(error) => {
self.note_problem(&format!("listing {key}"), &error);
break;
}
};
match parse_tree(&body) {
Ok(page_names) => {
let short = page_names.len() < 100;
names.extend(page_names);
if short {
whole = true;
break;
}
}
Err(error) => {
self.note_problem(&format!("listing {key}"), &error);
break;
}
}
}
if whole {
if let Ok(mut map) = self.listings.lock() {
map.insert(key, names.clone());
}
}
names
}
fn locate(&self, path: &str) -> Option<(Repo, String)> {
let path = path.replace('\\', "/");
let root = self.root.to_string_lossy().replace('\\', "/");
let rest = path.strip_prefix(&root)?.trim_start_matches('/');
for repo in [Repo::Symbols, Repo::Footprints, Repo::Packages3D] {
if let Some(inside) = rest.strip_prefix(repo.folder()) {
let inside = inside.trim_start_matches('/');
return Some((repo, inside.to_owned()));
}
}
None
}
}
impl KicadFiles for RemoteFiles {
fn read(&self, path: &str) -> Option<Vec<u8>> {
let (repo, inside) = self.locate(path)?;
if inside.is_empty() {
return None;
}
self.fetch_file(repo, &inside).ok()
}
fn is_file(&self, path: &str) -> bool {
let Some((repo, inside)) = self.locate(path) else {
return false;
};
if self.path_of(repo).join(&inside).is_file() {
return true;
}
let (dir, name) = inside.rsplit_once('/').unwrap_or(("", inside.as_str()));
self.tree(repo, dir).iter().any(|entry| entry == name)
}
fn is_dir(&self, path: &str) -> bool {
let Some((repo, inside)) = self.locate(path) else {
return false;
};
if inside.is_empty() || self.path_of(repo).join(&inside).is_dir() {
return true;
}
let (dir, name) = inside.rsplit_once('/').unwrap_or(("", inside.as_str()));
self.tree(repo, dir).iter().any(|entry| entry == name)
}
fn list(&self, dir: &str) -> Vec<String> {
let Some((repo, inside)) = self.locate(dir) else {
return Vec::new();
};
if repo == Repo::Footprints && inside.is_empty() {
if let Some(library) = &self.scope {
return vec![library.clone()];
}
}
self.tree(repo, &inside)
}
}
pub struct PartReader {
answer: std::sync::mpsc::Receiver<(KicadPart, Vec<String>)>,
}
impl PartReader {
pub fn take(&self) -> Option<(KicadPart, Vec<String>)> {
match self.answer.try_recv() {
Ok(answer) => Some(answer),
Err(std::sync::mpsc::TryRecvError::Empty) => None,
Err(std::sync::mpsc::TryRecvError::Disconnected) => Some((
KicadPart {
notes: vec!["the KiCad download stopped without an answer".into()],
..KicadPart::default()
},
Vec::new(),
)),
}
}
}
pub trait PartSource: KicadFiles + Send + 'static {
fn library(&self) -> KicadLibrary;
fn downloaded(&self) -> Vec<String> {
Vec::new()
}
}
impl PartSource for RemoteFiles {
fn library(&self) -> KicadLibrary {
RemoteFiles::library(self)
}
fn downloaded(&self) -> Vec<String> {
self.fetched()
}
}
pub fn read_part(symbol: Symbol, choice: FootprintChoice, source: impl PartSource) -> PartReader {
let (send, answer) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let library = source.library();
let part = part_from_symbol(symbol, &choice, &library, &source).parsed();
let _ = send.send((part, source.downloaded()));
});
PartReader { answer }
}
pub fn default_cache_root() -> PathBuf {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
.unwrap_or_else(|| PathBuf::from("."));
base.join("brep-app").join("kicad-library")
}