feat: image variants engine
This commit is contained in:
1 parent
f0845de643
commit
f1d56ef50e
80 files changed
+7312
-84
No files matched your search
@@ -0,0 +1,264 @@
|
||||
package imagevariant
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
validatorlib "github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
list *useCases.ListVariantsUseCase
|
||||
create *useCases.CreateVariantUseCase
|
||||
update *useCases.UpdateVariantUseCase
|
||||
setEnabled *useCases.SetVariantEnabledUseCase
|
||||
remove *useCases.DeleteVariantUseCase
|
||||
purge *useCases.PurgeVariantUseCase
|
||||
}
|
||||
|
||||
func NewHandler(list *useCases.ListVariantsUseCase, create *useCases.CreateVariantUseCase, update *useCases.UpdateVariantUseCase, setEnabled *useCases.SetVariantEnabledUseCase, remove *useCases.DeleteVariantUseCase, purge *useCases.PurgeVariantUseCase) *Handler {
|
||||
return &Handler{list, create, update, setEnabled, remove, purge}
|
||||
}
|
||||
|
||||
type definitionRequest struct {
|
||||
Width *int `json:"width" validate:"required_without=Height,omitempty,min=1,max=8000"`
|
||||
Height *int `json:"height" validate:"required_without=Width,omitempty,min=1,max=8000"`
|
||||
Fit string `json:"fit" validate:"omitempty,oneof=cover contain inside"`
|
||||
Format string `json:"format" validate:"omitempty,oneof=original jpeg png webp"`
|
||||
Quality *int `json:"quality" validate:"omitempty,min=1,max=100"`
|
||||
}
|
||||
|
||||
type switchesRequest struct {
|
||||
GenerateOnUpload *bool `json:"generate_on_upload"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type createRequest struct {
|
||||
Name string `json:"name" validate:"required,variantname"`
|
||||
definitionRequest
|
||||
switchesRequest
|
||||
}
|
||||
|
||||
type updateRequest struct {
|
||||
definitionRequest
|
||||
switchesRequest
|
||||
}
|
||||
|
||||
type enabledRequest struct {
|
||||
Enabled *bool `json:"enabled" validate:"required"`
|
||||
}
|
||||
|
||||
type variantResponse struct {
|
||||
Name string `json:"name"`
|
||||
Width *int `json:"width"`
|
||||
Height *int `json:"height"`
|
||||
Fit image.VariantFit `json:"fit"`
|
||||
Format image.VariantFormat `json:"format"`
|
||||
Quality *int `json:"quality"`
|
||||
GenerateOnUpload bool `json:"generate_on_upload"`
|
||||
Enabled bool `json:"enabled"`
|
||||
GeneratedFiles int `json:"generated_files"`
|
||||
CacheSize int64 `json:"cache_size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type updatedVariantResponse struct {
|
||||
variantResponse
|
||||
PurgedFiles int `json:"purged_files"`
|
||||
}
|
||||
|
||||
type listResponse struct {
|
||||
Items []variantResponse `json:"items"`
|
||||
}
|
||||
|
||||
type purgeResponse struct {
|
||||
PurgedFiles int `json:"purged_files"`
|
||||
PurgedBytes int64 `json:"purged_bytes"`
|
||||
}
|
||||
|
||||
func responseOf(summary useCases.VariantSummary) variantResponse {
|
||||
variant := summary.Variant
|
||||
return variantResponse{
|
||||
Name: variant.Name,
|
||||
Width: variant.Width,
|
||||
Height: variant.Height,
|
||||
Fit: variant.Fit,
|
||||
Format: variant.Format,
|
||||
Quality: variant.Quality,
|
||||
GenerateOnUpload: variant.GenerateOnUpload,
|
||||
Enabled: variant.Enabled,
|
||||
GeneratedFiles: summary.GeneratedFiles,
|
||||
CacheSize: summary.CacheSize,
|
||||
CreatedAt: variant.CreatedAt,
|
||||
UpdatedAt: variant.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) List(w http.ResponseWriter, _ *http.Request) {
|
||||
summaries, err := h.list.List()
|
||||
if err != nil {
|
||||
h.writeError(w, "", err)
|
||||
return
|
||||
}
|
||||
items := make([]variantResponse, 0, len(summaries))
|
||||
for _, summary := range summaries {
|
||||
items = append(items, responseOf(summary))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, listResponse{items})
|
||||
}
|
||||
|
||||
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var request createRequest
|
||||
if !h.decode(w, r, &request) {
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.create.Create(r.Context(), useCases.CreateVariantCommand{
|
||||
Name: request.Name,
|
||||
VariantDefinitionCommand: request.definitionRequest.command(),
|
||||
GenerateOnUpload: request.GenerateOnUpload,
|
||||
Enabled: request.Enabled,
|
||||
})
|
||||
if err != nil {
|
||||
h.writeError(w, request.Name, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, responseOf(summary))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
var request updateRequest
|
||||
if !h.decode(w, r, &request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
result, err := h.update.Update(r.Context(), useCases.UpdateVariantCommand{
|
||||
Name: name,
|
||||
VariantDefinitionCommand: request.definitionRequest.command(),
|
||||
GenerateOnUpload: request.GenerateOnUpload,
|
||||
Enabled: request.Enabled,
|
||||
})
|
||||
if err != nil {
|
||||
h.writeError(w, name, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, updatedVariantResponse{responseOf(result.VariantSummary), result.PurgedFiles})
|
||||
}
|
||||
|
||||
func (h *Handler) SetEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
var request enabledRequest
|
||||
if !h.decode(w, r, &request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
summary, err := h.setEnabled.SetEnabled(r.Context(), name, *request.Enabled)
|
||||
if err != nil {
|
||||
h.writeError(w, name, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, responseOf(summary))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
if err := h.remove.Delete(r.Context(), name); err != nil {
|
||||
h.writeError(w, name, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) Purge(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
purged, err := h.purge.Purge(r.Context(), name)
|
||||
if err != nil {
|
||||
h.writeError(w, name, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, purgeResponse{purged.Files, purged.Bytes})
|
||||
}
|
||||
|
||||
func (d definitionRequest) command() useCases.VariantDefinitionCommand {
|
||||
return useCases.VariantDefinitionCommand{Width: d.Width, Height: d.Height, Fit: d.Fit, Format: d.Format, Quality: d.Quality}
|
||||
}
|
||||
|
||||
func (h *Handler) decode(w http.ResponseWriter, r *http.Request, request any) bool {
|
||||
if err := json.NewDecoder(r.Body).Decode(request); err != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if err := validator.Validate.Struct(request); err != nil {
|
||||
http.Error(w, h.validationMessage(err), http.StatusUnprocessableEntity)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) validationMessage(err error) string {
|
||||
var failures validatorlib.ValidationErrors
|
||||
if !errors.As(err, &failures) || len(failures) == 0 {
|
||||
return apperr.VariantInvalidErrorMessage
|
||||
}
|
||||
switch failures[0].StructField() {
|
||||
case "Name":
|
||||
return apperr.VariantNameErrorMessage
|
||||
case "Width", "Height":
|
||||
return apperr.VariantDimensionsErrorMessage
|
||||
case "Fit":
|
||||
return apperr.VariantFitErrorMessage
|
||||
case "Format":
|
||||
return h.formatMessage()
|
||||
case "Quality":
|
||||
return apperr.VariantQualityErrorMessage
|
||||
default:
|
||||
return apperr.VariantInvalidErrorMessage
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) formatMessage() string {
|
||||
encodable := h.create.EncodableFormats()
|
||||
names := make([]string, 0, len(encodable))
|
||||
for _, format := range encodable {
|
||||
names = append(names, string(format))
|
||||
}
|
||||
last := len(names) - 1
|
||||
return fmt.Sprintf(apperr.VariantFormatErrorMessageFormat, strings.Join(names[:last], ", ")+" or "+names[last])
|
||||
}
|
||||
|
||||
func (h *Handler) writeError(w http.ResponseWriter, name string, err error) {
|
||||
switch {
|
||||
case errors.Is(err, image.ErrVariantNotFound):
|
||||
http.Error(w, apperr.VariantNotFoundErrorMessage, http.StatusNotFound)
|
||||
case errors.Is(err, image.ErrVariantExists):
|
||||
http.Error(w, fmt.Sprintf(apperr.VariantExistsErrorMessageFormat, name), http.StatusConflict)
|
||||
case errors.Is(err, image.ErrVariantFormatUnavailable):
|
||||
http.Error(w, h.formatMessage(), http.StatusUnprocessableEntity)
|
||||
case errors.Is(err, image.ErrVariantInvalid):
|
||||
http.Error(w, apperr.VariantInvalidErrorMessage, http.StatusUnprocessableEntity)
|
||||
default:
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
body, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
Reference in new issue
Block a user