Fix 5 high-risk bugs from 13th audit pass
- cleanup_placeholder now removes 1-byte marker files on error (regression)
- {width}/{height} template vars replaced with "0" when dimensions unavailable
- Text watermark width multiplier 0.6->1.0 to prevent CJK/wide char clipping
- cmd_undo preserves history entries when trash operations fail
- cmd_watch_remove exits on write failure instead of silent discard
This commit is contained in:
@@ -535,13 +535,15 @@ fn cmd_undo(count: usize) {
|
|||||||
let undo_count = count.min(entries.len());
|
let undo_count = count.min(entries.len());
|
||||||
let to_undo = entries.split_off(entries.len() - undo_count);
|
let to_undo = entries.split_off(entries.len() - undo_count);
|
||||||
let mut total_trashed = 0;
|
let mut total_trashed = 0;
|
||||||
|
let mut failed_entries = Vec::new();
|
||||||
|
|
||||||
for entry in &to_undo {
|
for entry in to_undo {
|
||||||
if entry.output_files.is_empty() {
|
if entry.output_files.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"Batch from {} has no recorded output files - cannot undo",
|
"Batch from {} has no recorded output files - cannot undo",
|
||||||
entry.timestamp
|
entry.timestamp
|
||||||
);
|
);
|
||||||
|
failed_entries.push(entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,11 +552,13 @@ fn cmd_undo(count: usize) {
|
|||||||
entry.total, entry.input_dir
|
entry.total, entry.input_dir
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut batch_trashed = 0;
|
||||||
for file_path in &entry.output_files {
|
for file_path in &entry.output_files {
|
||||||
let path = PathBuf::from(file_path);
|
let path = PathBuf::from(file_path);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
match trash::delete(&path) {
|
match trash::delete(&path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
batch_trashed += 1;
|
||||||
total_trashed += 1;
|
total_trashed += 1;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -563,9 +567,17 @@ fn cmd_undo(count: usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep entry in history if no files were trashed
|
||||||
|
if batch_trashed == 0 {
|
||||||
|
failed_entries.push(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove undone entries from history
|
// Re-add entries where trash failed so they can be retried
|
||||||
|
entries.extend(failed_entries);
|
||||||
|
|
||||||
|
// Write updated history
|
||||||
if let Err(e) = history.write_all(&entries) {
|
if let Err(e) = history.write_all(&entries) {
|
||||||
eprintln!("Warning: failed to update history after undo: {}", e);
|
eprintln!("Warning: failed to update history after undo: {}", e);
|
||||||
}
|
}
|
||||||
@@ -706,8 +718,17 @@ fn cmd_watch_remove(path: &str) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(json) = serde_json::to_string_pretty(&watches) {
|
match serde_json::to_string_pretty(&watches) {
|
||||||
let _ = std::fs::write(&watches_path, json);
|
Ok(json) => {
|
||||||
|
if let Err(e) = std::fs::write(&watches_path, json) {
|
||||||
|
eprintln!("Failed to write watch config: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to serialize watch config: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println!("Removed watch folder: {}", path);
|
println!("Removed watch folder: {}", path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -723,10 +723,11 @@ fn paths_are_same(a: &std::path::Path, b: &std::path::Path) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a 0-byte placeholder file created by find_unique_path on error.
|
/// Remove a placeholder file created by find_unique_path on error.
|
||||||
|
/// Placeholders are either 0-byte (no marker) or 1-byte (marker `~`).
|
||||||
fn cleanup_placeholder(path: &std::path::Path) {
|
fn cleanup_placeholder(path: &std::path::Path) {
|
||||||
if let Ok(meta) = std::fs::metadata(path) {
|
if let Ok(meta) = std::fs::metadata(path) {
|
||||||
if meta.len() == 0 {
|
if meta.len() <= 1 {
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,15 @@ pub fn apply_template_full(
|
|||||||
result = result.replace("{counter}", &counter.to_string());
|
result = result.replace("{counter}", &counter.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((w, h)) = dimensions {
|
match dimensions {
|
||||||
result = result.replace("{width}", &w.to_string());
|
Some((w, h)) => {
|
||||||
result = result.replace("{height}", &h.to_string());
|
result = result.replace("{width}", &w.to_string());
|
||||||
|
result = result.replace("{height}", &h.to_string());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
result = result.replace("{width}", "0");
|
||||||
|
result = result.replace("{height}", "0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// {date} - today's date
|
// {date} - today's date
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ fn render_text_to_image(
|
|||||||
opacity: f32,
|
opacity: f32,
|
||||||
) -> image::RgbaImage {
|
) -> image::RgbaImage {
|
||||||
let scale = ab_glyph::PxScale::from(font_size);
|
let scale = ab_glyph::PxScale::from(font_size);
|
||||||
let text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 0.6) as u32).saturating_add(4).min(8192);
|
let text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 1.0) as u32).saturating_add(4).min(16384);
|
||||||
let text_height = ((font_size.min(1000.0) * 1.4) as u32).saturating_add(4).min(4096);
|
let text_height = ((font_size.min(1000.0) * 1.4) as u32).saturating_add(4).min(4096);
|
||||||
|
|
||||||
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
||||||
@@ -266,7 +266,7 @@ fn apply_text_watermark(
|
|||||||
} else {
|
} else {
|
||||||
// No rotation - draw text directly (faster)
|
// No rotation - draw text directly (faster)
|
||||||
let scale = ab_glyph::PxScale::from(font_size);
|
let scale = ab_glyph::PxScale::from(font_size);
|
||||||
let text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 0.6) as u32).saturating_add(4).min(8192);
|
let text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 1.0) as u32).saturating_add(4).min(16384);
|
||||||
let text_height = ((font_size.min(1000.0) * 1.4) as u32).saturating_add(4).min(4096);
|
let text_height = ((font_size.min(1000.0) * 1.4) as u32).saturating_add(4).min(4096);
|
||||||
let text_dims = Dimensions {
|
let text_dims = Dimensions {
|
||||||
width: text_width,
|
width: text_width,
|
||||||
@@ -336,7 +336,7 @@ fn apply_tiled_text_watermark(
|
|||||||
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
||||||
let draw_color = Rgba([color[0], color[1], color[2], alpha]);
|
let draw_color = Rgba([color[0], color[1], color[2], alpha]);
|
||||||
|
|
||||||
let text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 0.6) as i64 + 4).min(8192);
|
let text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 1.0) as i64 + 4).min(16384);
|
||||||
let text_height = ((font_size.min(1000.0) * 1.4) as i64 + 4).min(4096);
|
let text_height = ((font_size.min(1000.0) * 1.4) as i64 + 4).min(4096);
|
||||||
|
|
||||||
let mut y = spacing as i64;
|
let mut y = spacing as i64;
|
||||||
|
|||||||
Reference in New Issue
Block a user