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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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,
|
mut post: Post,
|
||||||
) -> EngineResult<Post> {
|
) -> EngineResult<Post> {
|
||||||
let post_id = post.id.clone();
|
let post_id = post.id.clone();
|
||||||
|
let old_rel_path = post.file_path.clone();
|
||||||
|
|
||||||
// Compute file_path from created_at + slug.
|
// Compute file_path from created_at + slug.
|
||||||
let rel_path = post_file_path(post.created_at, &post.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);
|
let file_content = write_post_file(&post, &body);
|
||||||
atomic_write_str(&abs_path, &file_content)?;
|
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
|
// Set published snapshot fields
|
||||||
let tags_json = serde_json::to_string(&post.tags).unwrap_or_else(|_| "[]".into());
|
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"));
|
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]
|
#[test]
|
||||||
fn discard_post_draft_restores_published_state() {
|
fn discard_post_draft_restores_published_state() {
|
||||||
let (db, dir) = setup();
|
let (db, dir) = setup();
|
||||||
|
|||||||
@@ -10863,6 +10863,44 @@ mod tests {
|
|||||||
assert_eq!(refreshed.content, "Neuer Entwurf");
|
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]
|
#[test]
|
||||||
fn persist_post_editor_state_allows_published_posts_without_slug_conflict() {
|
fn persist_post_editor_state_allows_published_posts_without_slug_conflict() {
|
||||||
let (db, project, tmp) = setup();
|
let (db, project, tmp) = setup();
|
||||||
|
|||||||
Reference in New Issue
Block a user