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>
49 lines
2.2 KiB
Go
49 lines
2.2 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Attachment represents a file attached to an article.
|
|
//
|
|
// Lifecycle (plan A — upload-then-bind):
|
|
// - On the article-create page the article does not exist yet, so ArticleID
|
|
// is 0 and the row is temporarily owned by SessionToken (a random value
|
|
// generated for the page session).
|
|
// - When the article is saved, ArticleCreate binds pending rows by
|
|
// SessionToken, setting their ArticleID and clearing the token.
|
|
// - On the edit page uploads carry the real ArticleID directly.
|
|
//
|
|
// Disk deduplication: StoredName is the SHA-256 of the file content. Before
|
|
// writing, the handler checks whether a file with that name already exists on
|
|
// disk; if so it is reused (no rewrite). Deletion uses reference counting —
|
|
// the disk file is removed only when no Attachment rows reference it.
|
|
type Attachment struct {
|
|
ID uint `gorm:"primarykey" json:"id"`
|
|
ArticleID uint `gorm:"index" json:"article_id"` // 0 while pending on the create page
|
|
SessionToken string `gorm:"size:64;index" json:"-"` // temporary ownership token for the create page
|
|
UploaderID uint `gorm:"index" json:"uploader_id"`
|
|
Filename string `gorm:"size:255" json:"filename"` // original filename
|
|
StoredName string `gorm:"size:64;index" json:"stored_name"` // SHA-256 hex, the on-disk filename
|
|
Ext string `gorm:"size:32" json:"ext"`
|
|
MIME string `gorm:"size:128" json:"mime"`
|
|
Size int64 `gorm:"default:0" json:"size"`
|
|
Category string `gorm:"size:32" json:"category"` // image/document/archive/video/other
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
|
}
|
|
|
|
// TableName overrides the default GORM table name.
|
|
func (Attachment) TableName() string {
|
|
return "attachments"
|
|
}
|
|
|
|
// AttachmentCategoryImage reports whether this attachment is an image (used to
|
|
// decide markdown insertion form: ![]() vs []()).
|
|
func (a *Attachment) IsImage() bool {
|
|
return a.Category == CategoryImage
|
|
}
|