6b237a1417
Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
325 lines
7.7 KiB
Go
325 lines
7.7 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{}{})
|
|
}
|