fix: loading of data from the database seems to do something
This commit is contained in:
@@ -59,6 +59,14 @@ pub fn list_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::R
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn count_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<i64> {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM media WHERE project_id = ?1",
|
||||
params![project_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE media SET
|
||||
|
||||
@@ -328,6 +328,19 @@ pub fn rebuild_media_from_filesystem(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
) -> EngineResult<MediaRebuildReport> {
|
||||
rebuild_media_from_filesystem_with_progress(conn, data_dir, project_id, None)
|
||||
}
|
||||
|
||||
/// Per-item progress callback: (current_item, total_items, item_description).
|
||||
pub type ItemProgressFn = Box<dyn Fn(usize, usize, &str) + Send>;
|
||||
|
||||
/// Like `rebuild_media_from_filesystem` but with optional per-item progress.
|
||||
pub fn rebuild_media_from_filesystem_with_progress(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
on_item: Option<ItemProgressFn>,
|
||||
) -> EngineResult<MediaRebuildReport> {
|
||||
let mut report = MediaRebuildReport::default();
|
||||
let media_dir = data_dir.join("media");
|
||||
@@ -356,8 +369,6 @@ pub fn rebuild_media_from_filesystem(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if this is a translation sidecar: {name}.{lang}.meta
|
||||
// where lang is exactly 2 lowercase letters.
|
||||
if is_translation_sidecar(file_name) {
|
||||
translation_sidecars.push(path.to_path_buf());
|
||||
} else {
|
||||
@@ -365,8 +376,14 @@ pub fn rebuild_media_from_filesystem(
|
||||
}
|
||||
}
|
||||
|
||||
let total = canonical_sidecars.len() + translation_sidecars.len();
|
||||
|
||||
// Process canonical sidecars first
|
||||
for path in &canonical_sidecars {
|
||||
for (i, path) in canonical_sidecars.iter().enumerate() {
|
||||
if let Some(ref cb) = on_item {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?");
|
||||
cb(i + 1, total, name);
|
||||
}
|
||||
match rebuild_canonical_media(conn, data_dir, project_id, path) {
|
||||
Ok(created) => {
|
||||
if created {
|
||||
@@ -382,7 +399,12 @@ pub fn rebuild_media_from_filesystem(
|
||||
}
|
||||
|
||||
// Process translation sidecars
|
||||
for path in &translation_sidecars {
|
||||
let offset = canonical_sidecars.len();
|
||||
for (i, path) in translation_sidecars.iter().enumerate() {
|
||||
if let Some(ref cb) = on_item {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?");
|
||||
cb(offset + i + 1, total, name);
|
||||
}
|
||||
match rebuild_translation_sidecar(conn, data_dir, project_id, path) {
|
||||
Ok(created) => {
|
||||
if created {
|
||||
@@ -494,10 +516,9 @@ fn rebuild_canonical_media(
|
||||
sc.size
|
||||
};
|
||||
|
||||
// Get checksum (if file exists)
|
||||
// Get checksum (if file exists) — streamed to avoid loading large files into memory
|
||||
let checksum = if abs_file.exists() {
|
||||
let bytes = fs::read(&abs_file)?;
|
||||
Some(content_hash(&bytes))
|
||||
Some(crate::util::file_hash(&abs_file)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -451,6 +451,19 @@ pub fn rebuild_posts_from_filesystem(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
) -> EngineResult<RebuildReport> {
|
||||
rebuild_posts_from_filesystem_with_progress(conn, data_dir, project_id, None)
|
||||
}
|
||||
|
||||
/// Per-item progress callback: (current_item, total_items, item_description).
|
||||
pub type ItemProgressFn = Box<dyn Fn(usize, usize, &str) + Send>;
|
||||
|
||||
/// Like `rebuild_posts_from_filesystem` but with optional per-item progress.
|
||||
pub fn rebuild_posts_from_filesystem_with_progress(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
on_item: Option<ItemProgressFn>,
|
||||
) -> EngineResult<RebuildReport> {
|
||||
let mut report = RebuildReport::default();
|
||||
let posts_dir = data_dir.join("posts");
|
||||
@@ -477,8 +490,6 @@ pub fn rebuild_posts_from_filesystem(
|
||||
}
|
||||
|
||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
// Check if it's a translation: {slug}.{lang}.md
|
||||
// The stem would be {slug}.{lang}
|
||||
if is_translation_filename(stem) {
|
||||
translation_files.push(path.to_path_buf());
|
||||
} else {
|
||||
@@ -486,8 +497,14 @@ pub fn rebuild_posts_from_filesystem(
|
||||
}
|
||||
}
|
||||
|
||||
let total = canonical_files.len() + translation_files.len();
|
||||
|
||||
// Process canonical posts first
|
||||
for path in &canonical_files {
|
||||
for (i, path) in canonical_files.iter().enumerate() {
|
||||
if let Some(ref cb) = on_item {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?");
|
||||
cb(i + 1, total, name);
|
||||
}
|
||||
match rebuild_canonical_post(conn, data_dir, project_id, path) {
|
||||
Ok(created) => {
|
||||
if created {
|
||||
@@ -503,7 +520,12 @@ pub fn rebuild_posts_from_filesystem(
|
||||
}
|
||||
|
||||
// Process translations
|
||||
for path in &translation_files {
|
||||
let offset = canonical_files.len();
|
||||
for (i, path) in translation_files.iter().enumerate() {
|
||||
if let Some(ref cb) = on_item {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?");
|
||||
cb(offset + i + 1, total, name);
|
||||
}
|
||||
match rebuild_translation(conn, data_dir, project_id, path) {
|
||||
Ok(created) => {
|
||||
if created {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rusqlite::Connection;
|
||||
|
||||
@@ -27,6 +28,9 @@ pub struct FullRebuildReport {
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// Progress callback: (percent 0.0..1.0, phase description).
|
||||
pub type ProgressFn = Arc<dyn Fn(f32, &str) + Send + Sync>;
|
||||
|
||||
/// Orchestrate a full rebuild from filesystem into the database.
|
||||
///
|
||||
/// Ensures FTS tables exist, then rebuilds posts, media, templates, and scripts
|
||||
@@ -35,42 +39,89 @@ pub fn rebuild_from_filesystem(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
) -> EngineResult<FullRebuildReport> {
|
||||
rebuild_from_filesystem_with_progress(conn, data_dir, project_id, None)
|
||||
}
|
||||
|
||||
/// Like `rebuild_from_filesystem` but accepts an optional progress callback.
|
||||
pub fn rebuild_from_filesystem_with_progress(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
on_progress: Option<ProgressFn>,
|
||||
) -> EngineResult<FullRebuildReport> {
|
||||
let mut report = FullRebuildReport::default();
|
||||
let progress = |pct: f32, msg: &str| {
|
||||
if let Some(ref f) = on_progress {
|
||||
f(pct, msg);
|
||||
}
|
||||
};
|
||||
|
||||
// Phase weights: posts 0.0..0.35, media 0.35..0.70, templates 0.70..0.85, scripts 0.85..1.0
|
||||
|
||||
// 1. Ensure FTS tables exist
|
||||
progress(0.0, "Ensuring FTS tables...");
|
||||
fts::ensure_fts_tables(conn)?;
|
||||
|
||||
// 2. Rebuild posts
|
||||
let post_report = post::rebuild_posts_from_filesystem(conn, data_dir, project_id)?;
|
||||
// 2. Rebuild posts (0.00 .. 0.35)
|
||||
progress(0.01, "Scanning posts...");
|
||||
let post_item_cb: Option<post::ItemProgressFn> = on_progress.as_ref().map(|cb| {
|
||||
let cb = Arc::clone(cb);
|
||||
let f: post::ItemProgressFn = Box::new(move |current, total, name| {
|
||||
let phase_pct = if total > 0 { current as f32 / total as f32 } else { 1.0 };
|
||||
let global_pct = 0.01 + phase_pct * 0.34;
|
||||
let msg = format!("Posts: {current}/{total} \u{2014} {name}");
|
||||
cb(global_pct, &msg);
|
||||
});
|
||||
f
|
||||
});
|
||||
let post_report = post::rebuild_posts_from_filesystem_with_progress(
|
||||
conn, data_dir, project_id, post_item_cb,
|
||||
)?;
|
||||
report.posts_created = post_report.posts_created;
|
||||
report.posts_updated = post_report.posts_updated;
|
||||
report.translations_created = post_report.translations_created;
|
||||
report.translations_updated = post_report.translations_updated;
|
||||
report.errors.extend(post_report.errors);
|
||||
|
||||
// 3. Rebuild media
|
||||
let media_report = media::rebuild_media_from_filesystem(conn, data_dir, project_id)?;
|
||||
// 3. Rebuild media (0.35 .. 0.70)
|
||||
progress(0.35, "Scanning media...");
|
||||
let media_item_cb: Option<media::ItemProgressFn> = on_progress.as_ref().map(|cb| {
|
||||
let cb = Arc::clone(cb);
|
||||
let f: media::ItemProgressFn = Box::new(move |current, total, name| {
|
||||
let phase_pct = if total > 0 { current as f32 / total as f32 } else { 1.0 };
|
||||
let global_pct = 0.35 + phase_pct * 0.35;
|
||||
let msg = format!("Media: {current}/{total} \u{2014} {name}");
|
||||
cb(global_pct, &msg);
|
||||
});
|
||||
f
|
||||
});
|
||||
let media_report = media::rebuild_media_from_filesystem_with_progress(
|
||||
conn, data_dir, project_id, media_item_cb,
|
||||
)?;
|
||||
report.media_created = media_report.media_created;
|
||||
report.media_updated = media_report.media_updated;
|
||||
report.media_translations_created = media_report.translations_created;
|
||||
report.media_translations_updated = media_report.translations_updated;
|
||||
report.errors.extend(media_report.errors);
|
||||
|
||||
// 4. Rebuild templates
|
||||
// 4. Rebuild templates (0.70 .. 0.85)
|
||||
progress(0.70, "Rebuilding templates...");
|
||||
let tpl_report =
|
||||
template_rebuild::rebuild_templates_from_filesystem(conn, data_dir, project_id)?;
|
||||
report.templates_created = tpl_report.created;
|
||||
report.templates_updated = tpl_report.updated;
|
||||
report.errors.extend(tpl_report.errors);
|
||||
|
||||
// 5. Rebuild scripts
|
||||
// 5. Rebuild scripts (0.85 .. 1.0)
|
||||
progress(0.85, "Rebuilding scripts...");
|
||||
let script_report =
|
||||
script_rebuild::rebuild_scripts_from_filesystem(conn, data_dir, project_id)?;
|
||||
report.scripts_created = script_report.created;
|
||||
report.scripts_updated = script_report.updated;
|
||||
report.errors.extend(script_report.errors);
|
||||
|
||||
progress(1.0, "Rebuild complete");
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
/// Compute a hex-encoded SHA-256 hash of the given content.
|
||||
pub fn content_hash(content: &[u8]) -> String {
|
||||
@@ -8,6 +10,21 @@ pub fn content_hash(content: &[u8]) -> String {
|
||||
hex::encode(result)
|
||||
}
|
||||
|
||||
/// Compute a hex-encoded SHA-256 hash of a file by streaming (8 KB chunks).
|
||||
pub fn file_hash(path: &Path) -> std::io::Result<String> {
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buf = [0u8; 8192];
|
||||
loop {
|
||||
let n = file.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buf[..n]);
|
||||
}
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
}
|
||||
|
||||
// sha2 doesn't include hex encoding, so we use a tiny inline helper.
|
||||
mod hex {
|
||||
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod sidecar;
|
||||
pub mod thumbnail;
|
||||
|
||||
pub use slug::{slugify, ensure_unique};
|
||||
pub use checksum::content_hash;
|
||||
pub use checksum::{content_hash, file_hash};
|
||||
pub use timestamp::{unix_ms_to_iso, iso_to_unix_ms, year_month_from_unix_ms, year_month_day_from_unix_ms, now_unix_ms};
|
||||
pub use atomic_write::{atomic_write, atomic_write_str};
|
||||
pub use paths::*;
|
||||
|
||||
@@ -236,15 +236,16 @@ fn apply_orientation(img: DynamicImage, orientation: u16) -> DynamicImage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract image dimensions from a file.
|
||||
/// Extract image dimensions from a file (header-only, no full decode).
|
||||
pub fn image_dimensions(path: &Path) -> Result<(u32, u32), String> {
|
||||
let img = ImageReader::open(path)
|
||||
let reader = ImageReader::open(path)
|
||||
.map_err(|e| format!("open: {e}"))?
|
||||
.with_guessed_format()
|
||||
.map_err(|e| format!("format: {e}"))?
|
||||
.decode()
|
||||
.map_err(|e| format!("decode: {e}"))?;
|
||||
Ok(img.dimensions())
|
||||
.map_err(|e| format!("format: {e}"))?;
|
||||
let (w, h) = reader
|
||||
.into_dimensions()
|
||||
.map_err(|e| format!("dimensions: {e}"))?;
|
||||
Ok((w, h))
|
||||
}
|
||||
|
||||
/// Detect MIME type from file extension.
|
||||
|
||||
Reference in New Issue
Block a user