338 lines
9.3 KiB
Go
338 lines
9.3 KiB
Go
package article
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"mime/multipart"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"rijig/model"
|
|
"rijig/utils"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type ArticleService interface {
|
|
CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error)
|
|
GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error)
|
|
GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error)
|
|
UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error)
|
|
DeleteArticle(ctx context.Context, id string) error
|
|
}
|
|
|
|
type articleService struct {
|
|
articleRepo ArticleRepository
|
|
}
|
|
|
|
func NewArticleService(articleRepo ArticleRepository) ArticleService {
|
|
return &articleService{articleRepo}
|
|
}
|
|
|
|
func (s *articleService) transformToDTO(article *model.Article) (*ArticleResponseDTO, error) {
|
|
publishedAt, err := utils.FormatDateToIndonesianFormat(article.PublishedAt)
|
|
if err != nil {
|
|
publishedAt = ""
|
|
}
|
|
|
|
updatedAt, err := utils.FormatDateToIndonesianFormat(article.UpdatedAt)
|
|
if err != nil {
|
|
updatedAt = ""
|
|
}
|
|
|
|
return &ArticleResponseDTO{
|
|
ID: article.ID,
|
|
Title: article.Title,
|
|
CoverImage: article.CoverImage,
|
|
Author: article.Author,
|
|
Heading: article.Heading,
|
|
Content: article.Content,
|
|
PublishedAt: publishedAt,
|
|
UpdatedAt: updatedAt,
|
|
}, nil
|
|
}
|
|
|
|
func (s *articleService) transformToDTOs(articles []model.Article) ([]ArticleResponseDTO, error) {
|
|
var articleDTOs []ArticleResponseDTO
|
|
|
|
for _, article := range articles {
|
|
dto, err := s.transformToDTO(&article)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to transform article %s: %w", article.ID, err)
|
|
}
|
|
articleDTOs = append(articleDTOs, *dto)
|
|
}
|
|
|
|
return articleDTOs, nil
|
|
}
|
|
|
|
func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) {
|
|
if coverArticle == nil {
|
|
return "", fmt.Errorf("cover image is required")
|
|
}
|
|
|
|
pathImage := "/uploads/articles/"
|
|
coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage
|
|
|
|
if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil {
|
|
return "", fmt.Errorf("failed to create directory for cover article: %w", err)
|
|
}
|
|
|
|
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
|
|
extension := filepath.Ext(coverArticle.Filename)
|
|
if !allowedExtensions[extension] {
|
|
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
|
|
}
|
|
|
|
coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension)
|
|
coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName)
|
|
|
|
src, err := coverArticle.Open()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open uploaded file: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.Create(coverArticlePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create cover article file: %w", err)
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := dst.ReadFrom(src); err != nil {
|
|
return "", fmt.Errorf("failed to save cover article: %w", err)
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s", pathImage, coverArticleFileName), nil
|
|
}
|
|
|
|
func (s *articleService) deleteCoverArticle(imagePath string) error {
|
|
if imagePath == "" {
|
|
return nil
|
|
}
|
|
|
|
baseDir := "./public/" + os.Getenv("BASE_URL")
|
|
absolutePath := baseDir + imagePath
|
|
|
|
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
|
|
log.Printf("Image file not found (already deleted?): %s", absolutePath)
|
|
return nil
|
|
}
|
|
|
|
if err := os.Remove(absolutePath); err != nil {
|
|
return fmt.Errorf("failed to delete image: %w", err)
|
|
}
|
|
|
|
log.Printf("Image deleted successfully: %s", absolutePath)
|
|
return nil
|
|
}
|
|
|
|
func (s *articleService) invalidateArticleCache(articleID string) {
|
|
|
|
articleCacheKey := fmt.Sprintf("article:%s", articleID)
|
|
if err := utils.DeleteCache(articleCacheKey); err != nil {
|
|
log.Printf("Error deleting article cache: %v", err)
|
|
}
|
|
|
|
if err := utils.ScanAndDelete("articles:*"); err != nil {
|
|
log.Printf("Error deleting articles cache: %v", err)
|
|
}
|
|
}
|
|
|
|
func (s *articleService) CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) {
|
|
coverArticlePath, err := s.saveCoverArticle(coverImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to save cover image: %w", err)
|
|
}
|
|
|
|
article := model.Article{
|
|
Title: request.Title,
|
|
CoverImage: coverArticlePath,
|
|
Author: request.Author,
|
|
Heading: request.Heading,
|
|
Content: request.Content,
|
|
}
|
|
|
|
if err := s.articleRepo.CreateArticle(ctx, &article); err != nil {
|
|
|
|
if deleteErr := s.deleteCoverArticle(coverArticlePath); deleteErr != nil {
|
|
log.Printf("Failed to clean up image after create failure: %v", deleteErr)
|
|
}
|
|
return nil, fmt.Errorf("failed to create article: %w", err)
|
|
}
|
|
|
|
articleDTO, err := s.transformToDTO(&article)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to transform article: %w", err)
|
|
}
|
|
|
|
cacheKey := fmt.Sprintf("article:%s", article.ID)
|
|
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
|
|
log.Printf("Error caching article: %v", err)
|
|
}
|
|
|
|
s.invalidateArticleCache("")
|
|
|
|
return articleDTO, nil
|
|
}
|
|
|
|
func (s *articleService) GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error) {
|
|
|
|
var cacheKey string
|
|
if page <= 0 || limit <= 0 {
|
|
cacheKey = "articles:all"
|
|
} else {
|
|
cacheKey = fmt.Sprintf("articles:page:%d:limit:%d", page, limit)
|
|
}
|
|
|
|
type CachedArticlesData struct {
|
|
Articles []ArticleResponseDTO `json:"articles"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
var cachedData CachedArticlesData
|
|
if err := utils.GetCache(cacheKey, &cachedData); err == nil {
|
|
return cachedData.Articles, cachedData.Total, nil
|
|
}
|
|
|
|
articles, total, err := s.articleRepo.FindAllArticles(ctx, page, limit)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to fetch articles: %w", err)
|
|
}
|
|
|
|
articleDTOs, err := s.transformToDTOs(articles)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to transform articles: %w", err)
|
|
}
|
|
|
|
cacheData := CachedArticlesData{
|
|
Articles: articleDTOs,
|
|
Total: total,
|
|
}
|
|
if err := utils.SetCache(cacheKey, cacheData, time.Hour*24); err != nil {
|
|
log.Printf("Error caching articles: %v", err)
|
|
}
|
|
|
|
return articleDTOs, total, nil
|
|
}
|
|
|
|
func (s *articleService) GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("article ID cannot be empty")
|
|
}
|
|
|
|
cacheKey := fmt.Sprintf("article:%s", id)
|
|
|
|
var cachedArticle ArticleResponseDTO
|
|
if err := utils.GetCache(cacheKey, &cachedArticle); err == nil {
|
|
return &cachedArticle, nil
|
|
}
|
|
|
|
article, err := s.articleRepo.FindArticleByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch article: %w", err)
|
|
}
|
|
|
|
articleDTO, err := s.transformToDTO(article)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to transform article: %w", err)
|
|
}
|
|
|
|
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
|
|
log.Printf("Error caching article: %v", err)
|
|
}
|
|
|
|
return articleDTO, nil
|
|
}
|
|
|
|
func (s *articleService) UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("article ID cannot be empty")
|
|
}
|
|
|
|
existingArticle, err := s.articleRepo.FindArticleByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("article not found: %w", err)
|
|
}
|
|
|
|
oldCoverImage := existingArticle.CoverImage
|
|
var newCoverPath string
|
|
|
|
if coverImage != nil {
|
|
newCoverPath, err = s.saveCoverArticle(coverImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to save new cover image: %w", err)
|
|
}
|
|
}
|
|
|
|
updatedArticle := &model.Article{
|
|
Title: request.Title,
|
|
Author: request.Author,
|
|
Heading: request.Heading,
|
|
Content: request.Content,
|
|
CoverImage: existingArticle.CoverImage,
|
|
}
|
|
|
|
if newCoverPath != "" {
|
|
updatedArticle.CoverImage = newCoverPath
|
|
}
|
|
|
|
if err := s.articleRepo.UpdateArticle(ctx, id, updatedArticle); err != nil {
|
|
|
|
if newCoverPath != "" {
|
|
s.deleteCoverArticle(newCoverPath)
|
|
}
|
|
return nil, fmt.Errorf("failed to update article: %w", err)
|
|
}
|
|
|
|
if newCoverPath != "" && oldCoverImage != "" {
|
|
if err := s.deleteCoverArticle(oldCoverImage); err != nil {
|
|
log.Printf("Warning: failed to delete old cover image: %v", err)
|
|
}
|
|
}
|
|
|
|
article, err := s.articleRepo.FindArticleByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch updated article: %w", err)
|
|
}
|
|
|
|
articleDTO, err := s.transformToDTO(article)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to transform updated article: %w", err)
|
|
}
|
|
|
|
cacheKey := fmt.Sprintf("article:%s", id)
|
|
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
|
|
log.Printf("Error caching updated article: %v", err)
|
|
}
|
|
|
|
s.invalidateArticleCache(id)
|
|
|
|
return articleDTO, nil
|
|
}
|
|
|
|
func (s *articleService) DeleteArticle(ctx context.Context, id string) error {
|
|
if id == "" {
|
|
return fmt.Errorf("article ID cannot be empty")
|
|
}
|
|
|
|
article, err := s.articleRepo.FindArticleByID(ctx, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find article: %w", err)
|
|
}
|
|
|
|
if err := s.articleRepo.DeleteArticle(ctx, id); err != nil {
|
|
return fmt.Errorf("failed to delete article: %w", err)
|
|
}
|
|
|
|
if err := s.deleteCoverArticle(article.CoverImage); err != nil {
|
|
log.Printf("Warning: failed to delete cover image: %v", err)
|
|
}
|
|
|
|
s.invalidateArticleCache(id)
|
|
|
|
return nil
|
|
}
|