mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat: add batch image MVP
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -58,6 +58,7 @@ type Handlers struct {
|
||||
Payment *PaymentHandler
|
||||
PaymentWebhook *PaymentWebhookHandler
|
||||
AvailableChannel *AvailableChannelHandler
|
||||
BatchImage *BatchImageHandler
|
||||
}
|
||||
|
||||
// BuildInfo contains build-time information
|
||||
|
||||
@@ -115,6 +115,7 @@ func ProvideHandlers(
|
||||
paymentHandler *PaymentHandler,
|
||||
paymentWebhookHandler *PaymentWebhookHandler,
|
||||
availableChannelHandler *AvailableChannelHandler,
|
||||
batchImageHandler *BatchImageHandler,
|
||||
_ *service.IdempotencyCoordinator,
|
||||
_ *service.IdempotencyCleanupService,
|
||||
) *Handlers {
|
||||
@@ -135,6 +136,7 @@ func ProvideHandlers(
|
||||
Payment: paymentHandler,
|
||||
PaymentWebhook: paymentWebhookHandler,
|
||||
AvailableChannel: availableChannelHandler,
|
||||
BatchImage: batchImageHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +158,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewPaymentHandler,
|
||||
NewPaymentWebhookHandler,
|
||||
NewAvailableChannelHandler,
|
||||
NewBatchImageHandler,
|
||||
|
||||
// Admin handlers
|
||||
admin.NewDashboardHandler,
|
||||
|
||||
Reference in New Issue
Block a user