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 }