feat: article attachments with content-addressed dedup

Add an attachments table and AJAX upload/management on the article
create and edit pages.

- Attachment model with article_id (0 while pending on create),
  session_token ownership, and SHA-256 content-addressed stored_name
- Upload validates via the platform policy (switch/type/size) and
  deduplicates on disk by content hash
- Plan-A binding: attachments uploaded before an article exists are
  owned by a session token and bound to the new article on save
- Delete soft-removes the record and drops the disk file only when no
  remaining rows reference it (reference counting for deduped files)
- Edit page loads existing attachments via JSON list endpoint
- Row actions: insert into body (markdown image/link), set as cover
  (image only), and delete
- Download URLs use the configured default download base URL when set,
  else the local /uploads path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:02:09 +08:00
co-authored by Claude Fable 5
parent 0c5bcde7da
commit e872c08f3b
7 changed files with 486 additions and 13 deletions
+9
View File
@@ -68,6 +68,15 @@ func main() {
admin.POST("/articles/:id/delete", handlers.ArticleDelete(db))
}
// Protected article attachment routes (AJAX uploads / management).
attachments := router.Group("/admin/articles")
attachments.Use(middleware.AuthRequired())
{
attachments.POST("/attachments", handlers.UploadAttachment(db, cfg.Path))
attachments.POST("/attachments/:id/delete", handlers.DeleteAttachment(db, cfg.Path))
attachments.GET("/:id/attachments", handlers.ListAttachments(db))
}
// Protected admin settings routes (platform configuration).
settings := router.Group("/admin/settings")
settings.Use(middleware.AuthRequired())