Files
sub2api/backend/internal/handler/batch_image_handler.go
T
2026-07-04 05:30:50 +08:00

205 lines
5.9 KiB
Go

package handler
import (
"errors"
"io"
"net/http"
"strconv"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
type BatchImageHandler struct {
service *service.BatchImagePublicService
download *service.BatchImageDownloadService
cleanup *service.BatchImageCleanupService
}
func NewBatchImageHandler(service *service.BatchImagePublicService, download *service.BatchImageDownloadService, cleanup *service.BatchImageCleanupService) *BatchImageHandler {
return &BatchImageHandler{service: service, download: download, cleanup: cleanup}
}
func (h *BatchImageHandler) Submit(c *gin.Context) {
var req service.BatchImageSubmitRequest
if err := c.ShouldBindJSON(&req); err != nil {
batchImageError(c, service.ErrBatchImageInvalidItems)
return
}
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
got, err := h.service.Submit(c.Request.Context(), owner, req, c.GetHeader("Idempotency-Key"))
if err != nil {
batchImageError(c, err)
return
}
c.JSON(http.StatusOK, got)
}
func (h *BatchImageHandler) Get(c *gin.Context) {
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
got, err := h.service.Get(c.Request.Context(), owner, c.Param("id"))
if err != nil {
batchImageError(c, err)
return
}
c.JSON(http.StatusOK, got)
}
func (h *BatchImageHandler) Items(c *gin.Context) {
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
limit, _ := strconv.Atoi(c.Query("limit"))
got, err := h.service.ListItems(c.Request.Context(), owner, c.Param("id"), service.BatchImageItemsQuery{
Status: c.Query("status"),
Limit: limit,
Cursor: c.Query("cursor"),
})
if err != nil {
batchImageError(c, err)
return
}
c.JSON(http.StatusOK, got)
}
func (h *BatchImageHandler) Cancel(c *gin.Context) {
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
got, err := h.service.Cancel(c.Request.Context(), owner, c.Param("id"))
if err != nil {
batchImageError(c, err)
return
}
c.JSON(http.StatusOK, got)
}
func (h *BatchImageHandler) ItemContent(c *gin.Context) {
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
imageIndex := 0
if raw := c.Query("image_index"); raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil {
batchImageError(c, service.ErrBatchImageItemImageIndexOutOfRange)
return
}
imageIndex = parsed
}
stream, err := h.download.OpenItemContent(c.Request.Context(), owner, c.Param("id"), c.Param("custom_id"), imageIndex)
if err != nil {
batchImageError(c, err)
return
}
defer stream.Reader.Close()
c.Header("Content-Type", stream.ContentType)
c.Header("Content-Disposition", service.BatchImageContentDispositionAttachment(stream.Filename))
c.Header("Cache-Control", "private, max-age=300")
c.Header("X-Content-Type-Options", "nosniff")
if stream.ContentLength != nil && *stream.ContentLength >= 0 {
c.Header("Content-Length", strconv.FormatInt(*stream.ContentLength, 10))
}
c.Status(http.StatusOK)
if _, err := io.Copy(c.Writer, stream.Reader); err != nil {
return
}
}
func (h *BatchImageHandler) Download(c *gin.Context) {
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
maxItems, _ := strconv.Atoi(c.Query("max_items"))
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", service.BatchImageContentDispositionAttachment(c.Param("id")+".zip"))
c.Header("Cache-Control", "private, no-store")
c.Header("X-Content-Type-Options", "nosniff")
result, err := h.download.StreamZip(c.Request.Context(), owner, c.Param("id"), service.BatchImageZipOptions{
Status: c.Query("status"),
MaxItems: maxItems,
IncludeManifest: true,
}, c.Writer)
if err != nil {
if result == nil || c.Writer.Written() == false {
batchImageError(c, err)
}
return
}
}
func (h *BatchImageHandler) DeleteOutputs(c *gin.Context) {
owner, ok := batchImageOwnerFromContext(c)
if !ok {
batchImageError(c, infraerrors.New(http.StatusUnauthorized, "API_KEY_REQUIRED", "API key is required"))
return
}
got, err := h.cleanup.DeleteOutputsForOwner(c.Request.Context(), owner, c.Param("id"))
if err != nil {
batchImageError(c, err)
return
}
c.JSON(http.StatusOK, got)
}
func batchImageOwnerFromContext(c *gin.Context) (service.BatchImageOwner, bool) {
apiKey, ok := middleware.GetAPIKeyFromContext(c)
if !ok || apiKey == nil || apiKey.ID <= 0 || apiKey.UserID <= 0 {
return service.BatchImageOwner{}, false
}
return service.BatchImageOwner{
UserID: apiKey.UserID,
APIKeyID: apiKey.ID,
GroupID: apiKey.GroupID,
}, true
}
func batchImageError(c *gin.Context, err error) {
status := infraerrors.Code(err)
code := infraerrors.Reason(err)
message := infraerrors.Message(err)
if err == nil {
status = http.StatusInternalServerError
code = "INTERNAL_ERROR"
message = "internal error"
}
if status == 0 || status == http.StatusInternalServerError {
status = http.StatusInternalServerError
code = "INTERNAL_ERROR"
message = "internal error"
}
if errors.Is(err, service.ErrBatchImageJobNotFound) {
status = http.StatusNotFound
code = "BATCH_IMAGE_NOT_FOUND"
message = "batch image job not found"
}
c.JSON(status, gin.H{
"error": gin.H{
"type": "invalid_request_error",
"code": code,
"message": message,
},
})
}