372 lines
11 KiB
Go
372 lines
11 KiB
Go
package media
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/apperr"
|
|
"RenewCMS/api/pkg/claims"
|
|
"RenewCMS/api/pkg/upload"
|
|
validator "RenewCMS/api/pkg/validator"
|
|
domainImage "RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
type Handler struct {
|
|
CreateUseCase *useCases.CreateImageUseCase
|
|
ListUseCase *useCases.ListMediaUseCase
|
|
RenameUseCase *useCases.RenameMediaUseCase
|
|
DeleteUseCase *useCases.DeleteMediaUseCase
|
|
MergeUseCase *useCases.MergeMediaUseCase
|
|
UsageUseCase *useCases.GetMediaUsageUseCase
|
|
PurgeSettingsUseCase *useCases.GetPurgeSettingsUseCase
|
|
UpdatePurgeSettingsUseCase *useCases.UpdatePurgeSettingsUseCase
|
|
PreviewPurgeUseCase *useCases.PreviewPurgeUseCase
|
|
RunPurgeUseCase *useCases.RunPurgeUseCase
|
|
ListVariantsUseCase *useCases.ListImageVariantsUseCase
|
|
GenerateVariantUseCase *useCases.GenerateImageVariantUseCase
|
|
GenerateAllUseCase *useCases.GenerateAllImageVariantsUseCase
|
|
PurgeVariantUseCase *useCases.PurgeImageVariantUseCase
|
|
GenerateOnUploadUseCase *useCases.GenerateVariantsOnUploadUseCase
|
|
}
|
|
|
|
type RenameRequest struct {
|
|
Filename string `json:"filename" validate:"required"`
|
|
}
|
|
|
|
type MergeRequest struct {
|
|
KeepID uint32 `json:"keep_id" validate:"required,gt=0"`
|
|
MergeIDs []uint32 `json:"merge_ids" validate:"required,min=1,dive,gt=0"`
|
|
}
|
|
|
|
type mediaItemResponse struct {
|
|
ID uint32 `json:"id"`
|
|
Filename string `json:"filename"`
|
|
URL string `json:"url"`
|
|
Size int64 `json:"size"`
|
|
ContentType string `json:"content_type"`
|
|
Width *int `json:"width"`
|
|
Height *int `json:"height"`
|
|
SHA256 string `json:"sha256"`
|
|
UploadedBy string `json:"uploaded_by"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UsedIn int `json:"used_in"`
|
|
Duplicate bool `json:"duplicate"`
|
|
|
|
VariantCount int `json:"variant_count"`
|
|
VariantSize int64 `json:"variant_size"`
|
|
}
|
|
|
|
type countsResponse struct {
|
|
All int `json:"all"`
|
|
Unused int `json:"unused"`
|
|
Duplicates int `json:"duplicates"`
|
|
}
|
|
|
|
type listResponse struct {
|
|
Items []mediaItemResponse `json:"items"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
Size int `json:"size"`
|
|
Counts countsResponse `json:"counts"`
|
|
}
|
|
|
|
type pagingQuery struct {
|
|
Page int `validate:"gte=1"`
|
|
Size int `validate:"oneof=5 25 50 100"`
|
|
}
|
|
|
|
func intQueryParam(values url.Values, name string, fallback int) (int, error) {
|
|
if !values.Has(name) {
|
|
return fallback, nil
|
|
}
|
|
return strconv.Atoi(values.Get(name))
|
|
}
|
|
|
|
type usageResponse struct {
|
|
Type string `json:"type"`
|
|
ID uint32 `json:"id"`
|
|
Title string `json:"title"`
|
|
Published *bool `json:"published,omitempty"`
|
|
Fields []string `json:"fields"`
|
|
}
|
|
|
|
type mergeResponse struct {
|
|
ArticlesUpdated int `json:"articles_updated"`
|
|
SitesUpdated int `json:"sites_updated"`
|
|
AuthorsUpdated int `json:"authors_updated"`
|
|
Merged int `json:"merged"`
|
|
}
|
|
|
|
func NewHandler(create *useCases.CreateImageUseCase, list *useCases.ListMediaUseCase, rename *useCases.RenameMediaUseCase, deleteUseCase *useCases.DeleteMediaUseCase, merge *useCases.MergeMediaUseCase, usage *useCases.GetMediaUsageUseCase, purgeSettings *useCases.GetPurgeSettingsUseCase, updatePurgeSettings *useCases.UpdatePurgeSettingsUseCase, previewPurge *useCases.PreviewPurgeUseCase, runPurge *useCases.RunPurgeUseCase, listVariants *useCases.ListImageVariantsUseCase, generateVariant *useCases.GenerateImageVariantUseCase, generateAll *useCases.GenerateAllImageVariantsUseCase, purgeVariant *useCases.PurgeImageVariantUseCase, generateOnUpload *useCases.GenerateVariantsOnUploadUseCase) *Handler {
|
|
return &Handler{
|
|
CreateUseCase: create,
|
|
ListUseCase: list,
|
|
RenameUseCase: rename,
|
|
DeleteUseCase: deleteUseCase,
|
|
MergeUseCase: merge,
|
|
UsageUseCase: usage,
|
|
PurgeSettingsUseCase: purgeSettings,
|
|
UpdatePurgeSettingsUseCase: updatePurgeSettings,
|
|
PreviewPurgeUseCase: previewPurge,
|
|
RunPurgeUseCase: runPurge,
|
|
ListVariantsUseCase: listVariants,
|
|
GenerateVariantUseCase: generateVariant,
|
|
GenerateAllUseCase: generateAll,
|
|
PurgeVariantUseCase: purgeVariant,
|
|
GenerateOnUploadUseCase: generateOnUpload,
|
|
}
|
|
}
|
|
|
|
func writeInternalServerError(w http.ResponseWriter) {
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
writeInternalServerError(w)
|
|
return
|
|
}
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write(body)
|
|
}
|
|
|
|
func writeImageError(w http.ResponseWriter, err error) {
|
|
if errors.Is(err, domainImage.ErrImageNotFound) {
|
|
http.Error(w, apperr.ImageNotFoundErrorMessage, http.StatusNotFound)
|
|
return
|
|
}
|
|
writeInternalServerError(w)
|
|
}
|
|
|
|
func parseImageId(w http.ResponseWriter, r *http.Request) (uint32, bool) {
|
|
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
|
|
if err != nil {
|
|
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
|
|
return 0, false
|
|
}
|
|
return uint32(id), true
|
|
}
|
|
|
|
func dimension(value int) *int {
|
|
if value == 0 {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func toResponse(item useCases.MediaItem) mediaItemResponse {
|
|
return mediaItemResponse{
|
|
ID: item.Image.ID,
|
|
Filename: item.Image.Filename,
|
|
URL: item.Image.Path,
|
|
Size: item.Image.Size,
|
|
ContentType: item.Image.ContentType,
|
|
Width: dimension(item.Image.Width),
|
|
Height: dimension(item.Image.Height),
|
|
SHA256: item.Image.SHA256,
|
|
UploadedBy: item.UploadedBy,
|
|
CreatedAt: item.Image.CreatedAt,
|
|
UsedIn: item.UsedIn,
|
|
Duplicate: item.Duplicate,
|
|
|
|
VariantCount: item.VariantCount,
|
|
VariantSize: item.VariantSize,
|
|
}
|
|
}
|
|
|
|
func parseListQuery(r *http.Request) (domainImage.ListQuery, error) {
|
|
values, err := url.ParseQuery(r.URL.RawQuery)
|
|
if err != nil {
|
|
return domainImage.ListQuery{}, domainImage.ErrInvalidQuery
|
|
}
|
|
|
|
query := domainImage.NewListQuery()
|
|
query.Search = values.Get("q")
|
|
query.Hash = strings.ToLower(values.Get("hash"))
|
|
if values.Has("filter") {
|
|
query.Filter = domainImage.Filter(values.Get("filter"))
|
|
}
|
|
if values.Has("variants") {
|
|
if values.Get("variants") == "" {
|
|
return domainImage.ListQuery{}, domainImage.ErrInvalidQuery
|
|
}
|
|
query.Variants = domainImage.VariantsFilter(values.Get("variants"))
|
|
}
|
|
if values.Has("sort") {
|
|
query.Sort = domainImage.Sort(values.Get("sort"))
|
|
}
|
|
page, err := intQueryParam(values, "page", domainImage.DefaultListPage)
|
|
if err != nil {
|
|
return domainImage.ListQuery{}, domainImage.ErrInvalidQuery
|
|
}
|
|
size, err := intQueryParam(values, "size", domainImage.DefaultListSize)
|
|
if err != nil {
|
|
return domainImage.ListQuery{}, domainImage.ErrInvalidQuery
|
|
}
|
|
if validator.Validate.Struct(pagingQuery{Page: page, Size: size}) != nil {
|
|
return domainImage.ListQuery{}, domainImage.ErrInvalidQuery
|
|
}
|
|
query.Page = page
|
|
query.Size = size
|
|
return query, nil
|
|
}
|
|
|
|
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
|
query, err := parseListQuery(r)
|
|
if err != nil {
|
|
http.Error(w, apperr.MediaQueryErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
page, err := h.ListUseCase.List(query)
|
|
if err != nil {
|
|
if errors.Is(err, domainImage.ErrInvalidQuery) {
|
|
http.Error(w, apperr.MediaQueryErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeInternalServerError(w)
|
|
return
|
|
}
|
|
|
|
items := make([]mediaItemResponse, 0, len(page.Items))
|
|
for _, item := range page.Items {
|
|
items = append(items, toResponse(item))
|
|
}
|
|
response := listResponse{
|
|
Items: items,
|
|
Total: page.Total,
|
|
Page: page.Page,
|
|
Size: page.Size,
|
|
Counts: countsResponse{All: page.Counts.All, Unused: page.Counts.Unused, Duplicates: page.Counts.Duplicates},
|
|
}
|
|
writeJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
|
|
userId, _ := claims.UserId(r)
|
|
input, closeFile, ok := upload.ReadImageInput(w, r, userId)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer closeFile()
|
|
|
|
created, err := h.CreateUseCase.CreateImage(r.Context(), input)
|
|
if err != nil {
|
|
upload.WriteCreateError(w, err)
|
|
return
|
|
}
|
|
|
|
h.GenerateOnUploadUseCase.Generate(r.Context(), created)
|
|
h.writeItem(w, http.StatusCreated, created)
|
|
}
|
|
|
|
func (h *Handler) writeItem(w http.ResponseWriter, status int, img domainImage.Image) {
|
|
item, err := h.ListUseCase.Describe(img)
|
|
if err != nil {
|
|
writeInternalServerError(w)
|
|
return
|
|
}
|
|
writeJSON(w, status, toResponse(item))
|
|
}
|
|
|
|
func (h *Handler) Rename(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseImageId(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var request RenameRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := validator.Validate.Struct(request); err != nil {
|
|
http.Error(w, apperr.MediaFilenameErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
renamed, err := h.RenameUseCase.Rename(r.Context(), id, request.Filename)
|
|
if err != nil {
|
|
if errors.Is(err, domainImage.ErrInvalidFilename) {
|
|
http.Error(w, apperr.MediaFilenameErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
writeImageError(w, err)
|
|
return
|
|
}
|
|
|
|
h.writeItem(w, http.StatusOK, renamed)
|
|
}
|
|
|
|
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseImageId(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if err := h.DeleteUseCase.Delete(r.Context(), id); err != nil {
|
|
writeImageError(w, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) Usage(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseImageId(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
usages, err := h.UsageUseCase.Usage(id)
|
|
if err != nil {
|
|
writeImageError(w, err)
|
|
return
|
|
}
|
|
|
|
response := make([]usageResponse, 0, len(usages))
|
|
for _, usage := range usages {
|
|
response = append(response, usageResponse{Type: usage.Type, ID: usage.ID, Title: usage.Title, Published: usage.Published, Fields: usage.Fields})
|
|
}
|
|
writeJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
func (h *Handler) Merge(w http.ResponseWriter, r *http.Request) {
|
|
var request MergeRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
http.Error(w, apperr.MediaMergeRequestErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := validator.Validate.Struct(request); err != nil {
|
|
http.Error(w, apperr.MediaMergeRequestErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result, err := h.MergeUseCase.Merge(r.Context(), request.KeepID, request.MergeIDs)
|
|
if err != nil {
|
|
writeMergeError(w, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, mergeResponse{ArticlesUpdated: result.ArticlesUpdated, SitesUpdated: result.SitesUpdated, AuthorsUpdated: result.AuthorsUpdated, Merged: result.Merged})
|
|
}
|
|
|
|
func writeMergeError(w http.ResponseWriter, err error) {
|
|
switch {
|
|
case errors.Is(err, domainImage.ErrInvalidMergeRequest):
|
|
http.Error(w, apperr.MediaMergeRequestErrorMessage, http.StatusBadRequest)
|
|
case errors.Is(err, domainImage.ErrMergeCopyMissing), errors.Is(err, domainImage.ErrMergeHashMismatch):
|
|
http.Error(w, apperr.MediaMergeConflictErrorMessage, http.StatusConflict)
|
|
default:
|
|
writeImageError(w, err)
|
|
}
|
|
}
|