From 712d92b810933b307d24e385b25a68dccc3c1de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Erbsh=C3=A4u=C3=9Fer?= Date: Sun, 24 May 2026 09:22:32 +0200 Subject: [PATCH] add endpoints to get single blog article MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tobias Erbshäußer --- backend/blog.go | 52 +++++++++++++++++++++++-- backend/db.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/backend/blog.go b/backend/blog.go index 91e327e..07f30cb 100644 --- a/backend/blog.go +++ b/backend/blog.go @@ -32,8 +32,8 @@ type ArticleProperties struct { type Article struct { ArticleProperties - Content string - Files []ArticleFile + Content string `json:"content"` + Files []ArticleFile `json:"-"` } type ArticleFile struct { @@ -230,9 +230,53 @@ func (h *ApiHandler) ServeBlogPut(writer http.ResponseWriter, request *http.Requ } func (h *ApiHandler) ServeBlogGetSingle(writer http.ResponseWriter, request *http.Request) { - // TODO + 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) { - // TODO + 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) + } } diff --git a/backend/db.go b/backend/db.go index 1cd4ddb..aa05fa0 100644 --- a/backend/db.go +++ b/backend/db.go @@ -13,6 +13,8 @@ import ( _ "github.com/mattn/go-sqlite3" ) +var ErrNotFound = errors.New("not found") + type Database struct { db *sql.DB } @@ -203,6 +205,105 @@ func (db *Database) GetBlogArticles(showAll bool, offset int, limit int) ([]Arti return articles, nil } +func (db *Database) GetBlogArticle(showAll bool, id int64) (*Article, error) { + filter := "WHERE blog_article.id = ?" + if !showAll { + filter = filter + " AND status = " + strconv.Itoa(ArticleStatusPublished) + } + + statement := "SELECT blog_article.status, blog_article.title, blog_article.date, blog_article.modification_date, blog_article.content, blog_tag.name" + + " FROM blog_article" + + " LEFT JOIN blog_article_to_tag ON blog_article.id = blog_article_to_tag.article_id" + + " LEFT JOIN blog_tag ON blog_article_to_tag.tag_id = blog_tag.id" + + " " + filter + + " ORDER BY blog_tag.name" + + rows, err := db.db.Query(statement, id) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + if !rows.Next() { + return nil, ErrNotFound + } + + var status ArticleStatus + var title string + var dateStr string + var modificationDateStr *string + var content string + var tag *string + + err = rows.Scan(&status, &title, &dateStr, &modificationDateStr, &content, &tag) + if err != nil { + return nil, err + } + + date, _ := time.Parse(time.DateOnly, dateStr) + var modificationDate *time.Time + if modificationDateStr != nil { + tmp, _ := time.Parse(time.DateOnly, *modificationDateStr) + modificationDate = &tmp + } + + article := &Article{ + ArticleProperties{ + id, + title, + status, + make([]string, 0), + date, + modificationDate, + }, + content, + nil, + } + + if tag != nil { + article.Tags = append(article.Tags, *tag) + } + + for rows.Next() { + err = rows.Scan(&status, &title, &dateStr, &modificationDateStr, &content, &tag) + if err != nil { + return nil, err + } + + if tag != nil { + article.Tags = append(article.Tags, *tag) + } + } + + return article, nil +} + +func (db *Database) GetBlogArticleFile(showAll bool, articleId int64, fileId int64) (ArticleFile, error) { + filter := "WHERE blog_file.article_id = ? AND blog_file.id = ?" + if !showAll { + filter = filter + " AND blog_article.status = " + strconv.Itoa(ArticleStatusPublished) + } + + statement := "SELECT blog_file.data FROM blog_file" + + " INNER JOIN blog_article ON blog_article.id = blog_file.article_id" + + " " + filter + + var data []byte + err := db.db.QueryRow(statement, articleId, fileId).Scan(&data) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return ArticleFile{}, ErrNotFound + } + + return ArticleFile{}, err + } + + return ArticleFile{ + fileId, + data, + }, nil +} + func migrate(db *sql.DB, rootPassword string) error { tx, err := db.Begin() if err != nil {