Remove stale post files after path changes.

This commit is contained in:
2026-07-21 22:31:01 +02:00
parent 3920d6fc70
commit 53ada2c705
3 changed files with 139 additions and 1 deletions

View File

@@ -11,7 +11,7 @@ The project is under active development. Core blogging workflows are broadly ava
- Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
- Post, Liquid template, and Lua script editing with dedicated syntax highlighting and explicit syntax-check feedback, including publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync.
- SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, bDS2-compatible checksums and NFD slug generation, and FTS5 search.
- SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, stale post-path cleanup on republish, bDS2-compatible checksums and NFD slug generation, and FTS5 search.
- Optional on-device multilingual semantic search and similar-post tag suggestions backed by a persistent USearch index, plus always-available partial-name tag autocomplete and duplicate-post review in the desktop workspace.
- Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links.
- A localized OPML menu editor manages pages, submenus, and category archives with protected Home ordering, keyboard-accessible tree controls, drag-and-drop, and bDS2-compatible persistence.

View File

@@ -292,6 +292,7 @@ fn publish_post_in_savepoint(
mut post: Post,
) -> EngineResult<Post> {
let post_id = post.id.clone();
let old_rel_path = post.file_path.clone();
// Compute file_path from created_at + slug.
let rel_path = post_file_path(post.created_at, &post.slug);
@@ -318,6 +319,13 @@ fn publish_post_in_savepoint(
let file_content = write_post_file(&post, &body);
atomic_write_str(&abs_path, &file_content)?;
if !old_rel_path.is_empty() && old_rel_path != rel_path {
match fs::remove_file(data_dir.join(&old_rel_path)) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
// Set published snapshot fields
let tags_json = serde_json::to_string(&post.tags).unwrap_or_else(|_| "[]".into());
@@ -1866,6 +1874,98 @@ mod tests {
assert!(file_content.contains("Publish Me"));
}
#[test]
fn publish_replaces_divergent_post_path_and_ignores_missing_old_file() {
let (db, dir) = setup();
let mut post = create_post(
db.conn(),
dir.path(),
"p1",
"Moved Post",
Some("current body"),
vec![],
vec![],
None,
None,
None,
)
.unwrap();
post.file_path = "posts/legacy/moved-post.md".to_string();
qp::update_post(db.conn(), &post).unwrap();
let old_path = dir.path().join(&post.file_path);
fs::create_dir_all(old_path.parent().unwrap()).unwrap();
fs::write(&old_path, "stale legacy file").unwrap();
let published = publish_post(db.conn(), dir.path(), &post.id).unwrap();
assert_ne!(published.file_path, post.file_path);
assert!(!old_path.exists());
assert!(dir.path().join(&published.file_path).is_file());
assert_eq!(
qp::get_post_by_id(db.conn(), &post.id).unwrap().file_path,
published.file_path
);
let mut missing = create_post(
db.conn(),
dir.path(),
"p1",
"Missing Legacy Post",
Some("body"),
vec![],
vec![],
None,
None,
None,
)
.unwrap();
missing.file_path = "posts/legacy/already-gone.md".to_string();
qp::update_post(db.conn(), &missing).unwrap();
let published_missing = publish_post(db.conn(), dir.path(), &missing.id).unwrap();
assert!(dir.path().join(&published_missing.file_path).is_file());
}
#[test]
fn translation_publish_retains_divergent_old_file_like_bds2() {
let (db, dir) = setup();
let post = create_post(
db.conn(),
dir.path(),
"p1",
"Translated Move",
Some("body"),
vec![],
vec![],
None,
Some("en"),
None,
)
.unwrap();
let mut translation = upsert_translation(
db.conn(),
dir.path(),
&post.id,
"de",
"Verschoben",
None,
Some("Inhalt"),
)
.unwrap();
translation.file_path = "posts/legacy/translated-move.de.md".to_string();
qt::update_post_translation(db.conn(), &translation).unwrap();
let old_path = dir.path().join(&translation.file_path);
fs::create_dir_all(old_path.parent().unwrap()).unwrap();
fs::write(&old_path, "legacy translation").unwrap();
publish_post(db.conn(), dir.path(), &post.id).unwrap();
let published = qt::get_post_translation_by_id(db.conn(), &translation.id).unwrap();
assert_ne!(published.file_path, translation.file_path);
assert!(old_path.is_file());
assert!(dir.path().join(&published.file_path).is_file());
}
#[test]
fn discard_post_draft_restores_published_state() {
let (db, dir) = setup();

View File

@@ -10863,6 +10863,44 @@ mod tests {
assert_eq!(refreshed.content, "Neuer Entwurf");
}
#[test]
fn post_editor_publish_removes_a_divergent_old_post_file() {
let (db, project, tmp) = setup();
let mut created = post::create_post(
db.conn(),
tmp.path(),
&project.id,
"Moved Through Editor",
Some("Body"),
Vec::new(),
Vec::new(),
None,
Some("en"),
None,
)
.unwrap();
created.file_path = "posts/legacy/editor-old.md".to_string();
bds_core::db::queries::post::update_post(db.conn(), &created).unwrap();
let old_path = tmp.path().join(&created.file_path);
std::fs::create_dir_all(old_path.parent().unwrap()).unwrap();
std::fs::write(&old_path, "old file").unwrap();
let mut app = make_app(db, project, &tmp);
open_post_editor(&mut app, &created);
let _ = app.publish_post_editor(&created.id);
let published = bds_core::db::queries::post::get_post_by_id(
app.db.as_ref().unwrap().conn(),
&created.id,
)
.unwrap();
assert_eq!(published.status, PostStatus::Published);
assert_ne!(published.file_path, created.file_path);
assert!(!old_path.exists());
assert!(tmp.path().join(&published.file_path).is_file());
assert_eq!(app.post_editors[&created.id].status, PostStatus::Published);
}
#[test]
fn persist_post_editor_state_allows_published_posts_without_slug_conflict() {
let (db, project, tmp) = setup();