1use std::{collections::HashSet, fs::File, io, sync::mpsc, thread};
7
8use camino::Utf8Path;
9use diesel::prelude::*;
10use rayon::prelude::*;
11
12use crate::error::{AppError, AppResult};
13
14mod db;
15pub mod error;
16pub mod parser;
17pub mod scanner;
18mod schema;
19
20fn gather_and_clean_font_paths(
26 tx: &mut diesel::SqliteConnection,
27 font_root: &Utf8Path,
28) -> AppResult<Vec<String>> {
29 let db_paths: HashSet<String> = schema::font_files::table
31 .select(schema::font_files::path)
32 .load(tx)?
33 .into_iter()
34 .collect();
35
36 let disk_paths: HashSet<String> = scanner::scan_font_directory(font_root)
37 .map(|p| p.into_string())
38 .collect();
39
40 let to_delete: Vec<String> = db_paths.difference(&disk_paths).cloned().collect();
43 let to_add: Vec<String> = disk_paths.difference(&db_paths).cloned().collect();
45
46 for chunk in to_delete.chunks(10000) {
49 diesel::delete(schema::font_files::table.filter(schema::font_files::path.eq_any(chunk)))
50 .execute(tx)?;
51 }
52
53 diesel::sql_query(
55 "DELETE FROM font_family_names WHERE file_id NOT IN (SELECT id FROM font_files)",
56 )
57 .execute(tx)?;
58
59 Ok(to_add)
61}
62
63fn open_for_mmap(path: &str) -> io::Result<File> {
68 #[cfg(target_os = "windows")]
69 {
70 use std::os::windows::fs::OpenOptionsExt;
71
72 use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_SEQUENTIAL_SCAN;
73
74 std::fs::OpenOptions::new()
75 .read(true)
76 .custom_flags(FILE_FLAG_SEQUENTIAL_SCAN)
77 .open(path)
78 }
79 #[cfg(not(target_os = "windows"))]
80 {
81 let f = File::open(path)?;
82 #[cfg(target_os = "linux")]
83 unsafe {
84 use std::os::unix::io::AsRawFd;
85 libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_WILLNEED);
86 }
87 Ok(f)
88 }
89}
90
91pub fn update_font_database(font_root: &Utf8Path, db_url: &str) -> AppResult<()> {
99 let mut conn = db::initialize_db_connection(db_url)?;
100
101 conn.transaction::<_, AppError, _>(|tx| {
102 let new_paths = gather_and_clean_font_paths(tx, font_root)?;
103
104 if new_paths.is_empty() {
105 return Ok(());
106 }
107
108 let (sender, receiver) = mpsc::sync_channel(10000);
109
110 thread::scope(|s| -> AppResult<()> {
111 s.spawn(|| {
112 new_paths.into_par_iter().for_each_with(sender, |ch, path| {
113 if let Ok(data_file) = open_for_mmap(&path)
114 && let Ok(data) = unsafe { memmap2::Mmap::map(&data_file) }
115 {
116 let families = parser::get_font_family_names(&data);
117 let _ = ch.send((path, families.into_iter().collect::<Vec<_>>()));
119 }
120 });
121 });
122
123 for (path, families) in receiver {
124 let file_id: i32 = diesel::insert_into(schema::font_files::table)
126 .values(db::FontFile { path })
127 .returning(schema::font_files::id)
128 .get_result(tx)?;
129
130 for name in families {
132 diesel::insert_into(schema::font_family_names::table)
133 .values(db::FontFamilyName { file_id, name })
134 .execute(tx)?;
135 }
136 }
137
138 Ok(())
139 })?;
140
141 Ok(())
142 })
143}
144
145pub fn select_font_by_name(name: &str, db_url: &str) -> AppResult<Vec<String>> {
147 let mut conn = db::initialize_db_connection(db_url)?;
148 let fonts: Vec<String> = schema::font_files::table
149 .inner_join(
150 schema::font_family_names::table
151 .on(schema::font_files::id.eq(schema::font_family_names::file_id)),
152 )
153 .filter(schema::font_family_names::name.eq(name))
154 .select(schema::font_files::path)
155 .load(&mut conn)?;
156 Ok(fonts)
157}