blog/core/models/posts/posts.go

226 lines
5.1 KiB
Go

package posts
import (
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/kirsle/blog/core/jsondb"
"github.com/kirsle/golog"
)
// DB is a reference to the parent app's JsonDB object.
var DB *jsondb.DB
var log *golog.Logger
func init() {
log = golog.GetLogger("blog")
}
// Post holds information for a blog post.
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
Fragment string `json:"fragment"`
ContentType string `json:"contentType"`
AuthorID int `json:"author"`
Body string `json:"body,omitempty"`
Privacy string `json:"privacy"`
Sticky bool `json:"sticky"`
EnableComments bool `json:"enableComments"`
Tags []string `json:"tags"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ByFragment maps a blog post by its URL fragment.
type ByFragment struct {
ID int `json:"id"`
}
// New creates a blank post with sensible defaults.
func New() *Post {
return &Post{
ContentType: "markdown",
Privacy: "public",
EnableComments: true,
}
}
// ParseForm populates the post from form values.
func (p *Post) ParseForm(r *http.Request) {
id, _ := strconv.Atoi(r.FormValue("id"))
p.ID = id
p.Title = r.FormValue("title")
p.Fragment = r.FormValue("fragment")
p.ContentType = r.FormValue("content-type")
p.Body = r.FormValue("body")
p.Privacy = r.FormValue("privacy")
p.Sticky = r.FormValue("sticky") == "true"
p.EnableComments = r.FormValue("enable-comments") == "true"
// Ingest the tags.
tags := strings.Split(r.FormValue("tags"), ",")
p.Tags = []string{}
for _, tag := range tags {
p.Tags = append(p.Tags, strings.TrimSpace(tag))
}
}
// Validate makes sure the required fields are all present.
func (p *Post) Validate() error {
if p.Title == "" {
return errors.New("title is required")
}
if p.ContentType != "markdown" && p.ContentType != "markdown+html" &&
p.ContentType != "html" {
return errors.New("invalid setting for ContentType")
}
if p.Privacy != "public" && p.Privacy != "draft" && p.Privacy != "private" && p.Privacy != "unlisted" {
return errors.New("invalid setting for Privacy")
}
return nil
}
// Load a post by its ID.
func Load(id int) (*Post, error) {
p := &Post{}
err := DB.Get(fmt.Sprintf("blog/posts/%d", id), &p)
return p, err
}
// LoadFragment loads a blog entry by its URL fragment.
func LoadFragment(fragment string) (*Post, error) {
f := ByFragment{}
err := DB.Get("blog/fragments/"+fragment, &f)
if err != nil {
return nil, err
}
p, err := Load(f.ID)
return p, err
}
// Save the blog post.
func (p *Post) Save() error {
// Editing an existing post?
if p.ID == 0 {
p.ID = p.nextID()
}
// Generate a URL fragment if needed.
if p.Fragment == "" {
fragment := strings.ToLower(p.Title)
fragment = regexp.MustCompile(`[^A-Za-z0-9]+`).ReplaceAllString(fragment, "-")
if strings.Contains(fragment, "--") {
log.Error("Generated blog fragment '%s' contains double dashes still!", fragment)
}
p.Fragment = strings.Trim(fragment, "-")
// If still no fragment, make one based on the post ID.
if p.Fragment == "" {
p.Fragment = fmt.Sprintf("post-%d", p.ID)
}
}
// Make sure the URL fragment is unique!
if len(p.Fragment) > 0 {
if exist, err := LoadFragment(p.Fragment); err == nil && exist.ID != p.ID {
var resolved bool
for i := 1; i <= 100; i++ {
fragment := fmt.Sprintf("%s-%d", p.Fragment, i)
_, err := LoadFragment(fragment)
if err == nil {
continue
}
p.Fragment = fragment
resolved = true
break
}
if !resolved {
return fmt.Errorf("failed to generate a unique URL fragment for '%s' after 100 attempts", p.Fragment)
}
}
}
// Dates & times.
if p.Created.IsZero() {
p.Created = time.Now().UTC()
}
if p.Updated.IsZero() {
p.Updated = p.Created
}
// Empty tag lists.
if len(p.Tags) == 1 && p.Tags[0] == "" {
p.Tags = []string{}
}
// Write the post.
DB.Commit(fmt.Sprintf("blog/posts/%d", p.ID), p)
DB.Commit(fmt.Sprintf("blog/fragments/%s", p.Fragment), ByFragment{p.ID})
// Update the index cache.
err := UpdateIndex(p)
if err != nil {
return fmt.Errorf("RebuildIndex() error: %v", err)
}
// Clean up fragments.
CleanupFragments()
return nil
}
// Delete a blog entry.
func (p *Post) Delete() error {
if p.ID == 0 {
return errors.New("post has no ID")
}
// Delete the DB files.
DB.Delete(fmt.Sprintf("blog/posts/%d", p.ID))
DB.Delete(fmt.Sprintf("blog/fragments/%s", p.Fragment))
// Remove it from the index.
idx, err := GetIndex()
if err != nil {
return fmt.Errorf("GetIndex error: %v", err)
}
return idx.Delete(p)
}
// getNextID gets the next blog post ID.
func (p *Post) nextID() int {
// Highest ID seen so far.
var highest int
posts, err := DB.List("blog/posts")
if err != nil {
return 1
}
for _, doc := range posts {
fields := strings.Split(doc, "/")
id, err := strconv.Atoi(fields[len(fields)-1])
if err != nil {
continue
}
if id > highest {
highest = id
}
}
// Return the highest +1
return highest + 1
}