Remove stale post files after path changes.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user