Files
website/backend/blog.go
T
2026-05-24 09:40:23 +02:00

366 lines
8.6 KiB
Go

package main
import (
"archive/tar"
"backend/md"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
type ArticleStatus = int
const (
ArticleStatusDraft ArticleStatus = iota
ArticleStatusPublished
ArticleStatusOffline
)
type ArticleProperties struct {
Id int64 `json:"id"`
Title string `json:"title"`
Status ArticleStatus `json:"status"`
Tags []string `json:"tags"`
ReleaseDate time.Time `json:"date"`
ModificationDate *time.Time `json:"mod-date"`
}
type Article struct {
ArticleProperties
Content string `json:"content"`
Files []ArticleFile `json:"-"`
}
type ArticleFile struct {
Id int64
Data []byte
}
func ParseArticle(reader io.Reader, filePrefix string) (*Article, error) {
tarFiles := make(map[string][]byte)
tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
content, err := io.ReadAll(tarReader)
if err != nil {
return nil, err
}
tarFiles[header.Name] = content
}
readmeBytes, found := tarFiles["README.md"]
if !found {
return nil, errors.New("file 'README.md' not found")
}
usedFiles := make(map[string]ArticleFile)
prevFileId := int64(0)
pc, html, err := md.Parse(func(path string) (string, error) {
content, ok := tarFiles[strings.TrimPrefix(path, "./")]
if !ok {
return "", errors.New("file '" + path + "' not found")
}
prevFileId++
usedFiles[path] = ArticleFile{prevFileId, content}
return filePrefix + strconv.FormatInt(prevFileId, 10), nil
}, readmeBytes)
if err != nil {
return nil, err
}
title, err := md.GetProperty(pc, "title")
if err != nil {
if errors.Is(err, md.PropertyNotFoundError) {
return nil, errors.New("title property not found")
}
return nil, err
}
tagsStr, err := md.GetProperty(pc, "tags")
if err != nil && !errors.Is(err, md.PropertyNotFoundError) {
return nil, err
}
tags := make([]string, 0)
if strings.TrimSpace(tagsStr) != "" {
for _, tagStr := range strings.Split(tagsStr, ",") {
tags = append(tags, strings.TrimSpace(tagStr))
}
}
dateStr, err := md.GetProperty(pc, "date")
if err != nil {
if errors.Is(err, md.PropertyNotFoundError) {
return nil, errors.New("date property not found")
}
return nil, err
}
releaseDate, err := time.Parse(time.DateOnly, dateStr)
if err != nil {
return nil, fmt.Errorf("invalid date property '%s': %v", dateStr, err)
}
var modificationDate *time.Time
dateStr, err = md.GetProperty(pc, "mod-date")
if err == nil {
tmp, err := time.Parse(time.DateOnly, dateStr)
if err != nil {
return nil, fmt.Errorf("invalid mod-date property '%s': %v", dateStr, err)
}
modificationDate = &tmp
} else if !errors.Is(err, md.PropertyNotFoundError) {
return nil, err
}
files := make([]ArticleFile, 0, len(usedFiles))
for _, file := range usedFiles {
files = append(files, file)
}
return &Article{
ArticleProperties{
-1,
title,
ArticleStatusDraft,
tags,
releaseDate,
modificationDate,
},
string(html),
files,
}, nil
}
func (h *ApiHandler) ServeBlogGet(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
var err error
var offset int
var limit int
offsetStr := query.Get("offset")
if offsetStr == "" {
offset = 0
} else {
offset, err = strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
WriteError(writer, http.StatusBadRequest, "invalid offset", nil)
return
}
}
limitStr := query.Get("limit")
if limitStr == "" {
limit = 50
} else {
limit, err = strconv.Atoi(limitStr)
if err != nil || limit <= 0 || limit > 100 {
WriteError(writer, http.StatusBadRequest, "invalid limit", nil)
return
}
}
articles, err := h.db.GetBlogArticles(IsAuthorized(request), offset, limit)
if err != nil {
WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
return
}
WriteResponse(writer, http.StatusOK, articles)
}
func (h *ApiHandler) ServeBlogPut(writer http.ResponseWriter, request *http.Request) {
err := request.ParseMultipartForm(10 * 1024 * 1024)
if err != nil {
WriteError(writer, http.StatusBadRequest, "failed to parse multipart form", nil)
return
}
file, _, err := request.FormFile("file")
if err != nil {
WriteError(writer, http.StatusBadRequest, "failed to parse file", nil)
return
}
defer func() {
_ = file.Close()
}()
id, commit, err := h.db.CreateBlogArticle()
if err != nil {
WriteError(writer, http.StatusInternalServerError, "failed to write database", err)
return
}
article, err := ParseArticle(file, "/api/blog/"+strconv.FormatInt(id, 10)+"/file/")
if err != nil {
_ = commit(nil)
WriteError(writer, http.StatusInternalServerError, "failed to parse article: "+err.Error(), nil)
return
}
err = commit(article)
if err != nil {
WriteError(writer, http.StatusInternalServerError, "failed to write database", err)
return
}
log.Printf("created new blog article '%s'", article.Title)
WriteResponse(writer, http.StatusOK, map[string]interface{}{
"id": id,
})
}
func (h *ApiHandler) ServeBlogGetSingle(writer http.ResponseWriter, request *http.Request) {
idStr := request.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
WriteError(writer, http.StatusBadRequest, "invalid id", err)
return
}
article, err := h.db.GetBlogArticle(IsAuthorized(request), id)
if err != nil {
if errors.Is(err, ErrNotFound) {
WriteError(writer, http.StatusNotFound, "article not found", nil)
} else {
WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
}
return
}
WriteResponse(writer, http.StatusOK, article)
}
func (h *ApiHandler) ServeBlogFileGetSingle(writer http.ResponseWriter, request *http.Request) {
idStr := request.PathValue("articleId")
articleId, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
WriteError(writer, http.StatusBadRequest, "invalid article id", err)
return
}
idStr = request.PathValue("fileId")
fileId, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
WriteError(writer, http.StatusBadRequest, "invalid file id", err)
return
}
file, err := h.db.GetBlogArticleFile(IsAuthorized(request), articleId, fileId)
if err != nil {
if errors.Is(err, ErrNotFound) {
WriteError(writer, http.StatusNotFound, "article file not found", nil)
} else {
WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
}
return
}
_, err = writer.Write(file.Data)
if err != nil {
log.Println(err)
}
}
func (h *ApiHandler) ServeBlogPostPublish(writer http.ResponseWriter, request *http.Request) {
idStr := request.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
WriteError(writer, http.StatusBadRequest, "invalid id", err)
return
}
err = h.db.SetBlogArticleStatus(id, ArticleStatusPublished)
if err != nil {
if errors.Is(err, ErrNotFound) {
WriteError(writer, http.StatusNotFound, "article not found", nil)
} else {
WriteError(writer, http.StatusInternalServerError, "failed to update database", err)
}
return
}
WriteResponse(writer, http.StatusOK, map[string]interface{}{})
}
func (h *ApiHandler) ServeBlogPostUnpublish(writer http.ResponseWriter, request *http.Request) {
idStr := request.PathValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
WriteError(writer, http.StatusBadRequest, "invalid id", err)
return
}
err = h.db.SetBlogArticleStatus(id, ArticleStatusOffline)
if err != nil {
if errors.Is(err, ErrNotFound) {
WriteError(writer, http.StatusNotFound, "article not found", nil)
} else {
WriteError(writer, http.StatusInternalServerError, "failed to update database", err)
}
return
}
WriteResponse(writer, http.StatusOK, map[string]interface{}{})
}
func (h *ApiHandler) ServeBlogTagsGet(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
var err error
var offset int
var limit int
offsetStr := query.Get("offset")
if offsetStr == "" {
offset = 0
} else {
offset, err = strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
WriteError(writer, http.StatusBadRequest, "invalid offset", nil)
return
}
}
limitStr := query.Get("limit")
if limitStr == "" {
limit = 50
} else {
limit, err = strconv.Atoi(limitStr)
if err != nil || limit <= 0 || limit > 100 {
WriteError(writer, http.StatusBadRequest, "invalid limit", nil)
return
}
}
tags, total, err := h.db.GetBlogTags(IsAuthorized(request), offset, limit)
if err != nil {
WriteError(writer, http.StatusInternalServerError, "failed to query database", err)
return
}
WriteResponse(writer, http.StatusOK, map[string]interface{}{
"tags": tags,
"total": total,
})
}