Store article content outside of DB and convert and serve on request from respective file

This commit is contained in:
Jason Streifling 2024-08-30 23:43:01 +02:00
parent 3f1b18c29f
commit 4663cedec5
9 changed files with 146 additions and 104 deletions

View File

@ -12,8 +12,10 @@ type Article struct {
Title string Title string
Created time.Time Created time.Time
Description string Description string
Content string
Link string Link string
EncURL string
EncLength int
EncType string
Published bool Published bool
Rejected bool Rejected bool
ID int64 ID int64
@ -27,8 +29,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false" selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := ` insertQuery := `
INSERT INTO articles INSERT INTO articles
(title, description, content, link, published, rejected, author_id, issue_id) (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -45,8 +47,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
} }
result, err := tx.Exec(insertQuery, a.Title, a.Description, result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
a.Content, a.Link, a.Published, a.Rejected, a.AuthorID, id) a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id)
if err != nil { if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -80,7 +82,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetArticle(id int64) (*Article, error) {
query := ` query := `
SELECT title, created, description, content, link, published, author_id SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id
FROM articles FROM articles
WHERE id = ? WHERE id = ?
` `
@ -91,7 +93,8 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
var err error var err error
if err := row.Scan(&article.Title, &created, &article.Description, if err := row.Scan(&article.Title, &created, &article.Description,
&article.Content, &article.Link, &article.Published, &article.AuthorID); err != nil { &article.Link, &article.EncURL, &article.EncLength, &article.EncType,
&article.Published, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -106,7 +109,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := ` query := `
SELECT id, title, created, description, content, link, author_id, issue_id SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id
FROM articles FROM articles
WHERE published = ? WHERE published = ?
AND rejected = ? AND rejected = ?
@ -122,8 +125,8 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.Link, &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.AuthorID, &article.IssueID); err != nil { &article.EncType, &article.AuthorID, &article.IssueID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -144,7 +147,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false" issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := ` articlesQuery := `
SELECT id, title, created, description, content, link, author_id SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id
FROM articles FROM articles
WHERE issue_id = ? AND published = true WHERE issue_id = ? AND published = true
` `
@ -178,7 +181,8 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.Link, &article.AuthorID); err != nil { &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
} }

View File

@ -9,47 +9,6 @@ import (
"git.streifling.com/jason/rss" "git.streifling.com/jason/rss"
) )
func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
channel := &rss.Channel{
Title: title,
Link: link,
Description: description,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles(true, false)
if err != nil {
return nil, fmt.Errorf("error fetching published articles: %v", err)
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error fetching tags for article %v: %v", article.Title, err)
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
user, err := db.GetUser(article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error finding user %v: %v", article.AuthorID, err)
}
channel.Items = append(channel.Items, &rss.Item{
Title: article.Title,
Author: fmt.Sprint(user.FirstName, " ", user.LastName),
PubDate: article.Created.Format(time.RFC1123Z),
Description: article.Description,
Content: &rss.Content{Value: article.Content},
Categories: tagNames,
})
}
return channel, nil
}
func GenerateRSS(c *Config, db *DB) (*string, error) { func GenerateRSS(c *Config, db *DB) (*string, error) {
channel := &rss.Channel{ channel := &rss.Channel{
Title: c.Title, Title: c.Title,
@ -93,15 +52,19 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
Author: fmt.Sprint(user.FirstName, " ", user.LastName), Author: fmt.Sprint(user.FirstName, " ", user.LastName),
Categories: tagNames, Categories: tagNames,
Description: articleDescription, Description: articleDescription,
Guid: string(article.ID),
Link: article.Link,
PubDate: article.Created.Format(time.RFC1123Z), PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle, Title: articleTitle,
} }
fmt.Println(article.Link, ": ", len(article.Link)) fmt.Println(article.Link, ": ", len(article.Link))
if article.Link == "" { if article.Title == "Autogenerated cpolis Issue Article" {
item.Link = fmt.Sprint("http://", c.Domain, "/article/serve/", article.ID, ".html") item.Enclosure = &rss.Enclosure{
} else { Url: article.EncURL,
item.Link = article.Link Lenght: article.EncLength,
Type: article.EncType,
}
} }
channel.Items = append(channel.Items, item) channel.Items = append(channel.Items, item)

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
"strconv" "strconv"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
@ -30,6 +31,24 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return return
} }
fmt.Fprint(w, article.Content) if !article.Published {
return
}
contentBytes, err := os.ReadFile(article.Link)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, content)
} }
} }

View File

@ -75,7 +75,6 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"), Description: r.PostFormValue("article-description"),
Content: r.PostFormValue("article-content"),
Published: false, Published: false,
Rejected: false, Rejected: false,
AuthorID: session.Values["id"].(int64), AuthorID: session.Values["id"].(int64),
@ -88,6 +87,20 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint("http://", c.Domain, c.Port, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.ParseForm() r.ParseForm()
tags := make([]int64, 0) tags := make([]int64, 0)
for _, tag := range r.Form["tags"] { for _, tag := range r.Form["tags"] {
@ -129,10 +142,16 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
description := r.PostFormValue("article-description") description := r.PostFormValue("article-description")
content := r.PostFormValue("article-content") content := r.PostFormValue("article-content")
link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
if err = os.WriteFile(link, []byte(content), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes( if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description}, &b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
); err != nil { ); err != nil {
log.Println(err) log.Println(err)
@ -260,7 +279,15 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
content, err := b.ConvertToHTML(article.Content) articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -290,6 +317,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
type htmlData struct { type htmlData struct {
Selected map[int64]bool Selected map[int64]bool
Article *b.Article Article *b.Article
Content string
Tags []*b.Tag Tags []*b.Tag
} }
data := new(htmlData) data := new(htmlData)
@ -308,6 +336,16 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = string(contentBytes)
data.Tags, err = db.GetTagList() data.Tags, err = db.GetTagList()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -346,27 +384,6 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(id); err != nil { if err = db.AddArticleToCurrentIssue(id); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -555,7 +572,14 @@ func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
content, err := b.ConvertToHTML(article.Content) contentBytes, err := os.ReadFile(article.Link)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -5,6 +5,7 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"mime"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -36,10 +37,32 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return return
} }
imgFileName := session.Values["issue-image"].(string)
imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
imgFile, err := os.Open(imgAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer imgFile.Close()
imgInfo, err := imgFile.Stat()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
imgSize := imgInfo.Size()
mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
article := &b.Article{ article := &b.Article{
Title: "Autogenerated Issue Article", Title: "Autogenerated cpolis Issue Article",
Content: r.PostFormValue("issue-content"), EncURL: fmt.Sprint("http://", c.Domain, c.Port, "/image/serve/", imgFileName),
Link: session.Values["issue-image"].(string), EncLength: int(imgSize),
EncType: mimeType,
Published: true, Published: true,
Rejected: false, Rejected: false,
Created: time.Now(), Created: time.Now(),
@ -47,20 +70,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
} }
fmt.Println(article.Link) fmt.Println(article.Link)
content, err := b.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -68,6 +77,26 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint("http://", c.Domain, c.Port, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(article.ID); err != nil { if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -26,6 +26,9 @@ CREATE TABLE articles (
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL, description TEXT NOT NULL,
link VARCHAR(255), link VARCHAR(255),
enc_url VARCHAR(255),
enc_length INT,
enc_type VARCHAR(255),
published BOOL NOT NULL, published BOOL NOT NULL,
rejected BOOL NOT NULL, rejected BOOL NOT NULL,
author_id INT NOT NULL, author_id INT NOT NULL,

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.22.0
require ( require (
firebase.google.com/go/v4 v4.14.1 firebase.google.com/go/v4 v4.14.1
git.streifling.com/jason/rss v0.1.2 git.streifling.com/jason/rss v0.1.3
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0

4
go.sum
View File

@ -15,8 +15,8 @@ cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJah
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4= git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg= git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=

View File

@ -14,7 +14,7 @@
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label> <label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Article.Content}}</textarea> <textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" /> <input id="article-content" name="article-content" type="hidden" />
</div> </div>