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;
16mod parser;
17mod scanner;
18mod schema;
19
20pub fn 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
63pub fn 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<usize> {
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 let new_paths_len = new_paths.len();
104
105 if new_paths.is_empty() {
106 return Ok(0);
107 }
108
109 let (sender, receiver) = mpsc::sync_channel(10000);
110
111 thread::scope(|s| -> AppResult<()> {
112 s.spawn(|| {
113 new_paths.into_par_iter().for_each_with(sender, |ch, path| {
114 if let Ok(data_file) = open_for_mmap(&path)
115 && let Ok(data) = unsafe { memmap2::Mmap::map(&data_file) }
116 {
117 let families = parser::get_font_family_names(&data);
118 let _ = ch.send((path, families.into_iter().collect::<Vec<_>>()));
120 }
121 });
122 });
123
124 for (path, families) in receiver {
125 let file_id: i32 = diesel::insert_into(schema::font_files::table)
127 .values(db::FontFile { path })
128 .returning(schema::font_files::id)
129 .get_result(tx)?;
130
131 for name in families {
133 diesel::insert_into(schema::font_family_names::table)
134 .values(db::FontFamilyName { file_id, name })
135 .execute(tx)?;
136 }
137 }
138
139 Ok(())
140 })?;
141
142 Ok(new_paths_len)
143 })
144}
145
146pub fn select_font_by_name(name: &str, db_url: &str) -> AppResult<Vec<String>> {
148 let mut conn = db::initialize_db_connection(db_url)?;
149 let fonts: Vec<String> = schema::font_files::table
150 .inner_join(
151 schema::font_family_names::table
152 .on(schema::font_files::id.eq(schema::font_family_names::file_id)),
153 )
154 .filter(schema::font_family_names::name.eq(name))
155 .select(schema::font_files::path)
156 .load(&mut conn)?;
157 Ok(fonts)
158}