package server import ( "fmt" "io" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" ) // APIResponse represents the unified JSON structure for all API responses. type APIResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } // OK responds with 200 and success data. func OK(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, APIResponse{Success: true, Data: data}) } // Created responds with 201 and success data. func Created(c *gin.Context, data interface{}) { c.JSON(http.StatusCreated, APIResponse{Success: true, Data: data}) } // BadRequest responds with 400 and an error message. func BadRequest(c *gin.Context, msg string) { c.JSON(http.StatusBadRequest, APIResponse{Success: false, Error: msg}) } // NotFound responds with 404 and an error message. func NotFound(c *gin.Context, msg string) { c.JSON(http.StatusNotFound, APIResponse{Success: false, Error: msg}) } // InternalError responds with 500 and an error message. func InternalError(c *gin.Context, msg string) { c.JSON(http.StatusInternalServerError, APIResponse{Success: false, Error: msg}) } // ErrorWithStatus responds with a custom status code and an error message. func ErrorWithStatus(c *gin.Context, code int, msg string) { c.JSON(code, APIResponse{Success: false, Error: msg}) } // ParseRange parses an HTTP Range header (RFC 7233). // Only single-part ranges are supported: bytes=start-end, bytes=start-, bytes=-suffix. // Multi-part ranges (bytes=0-100,200-300) return an error. func ParseRange(rangeHeader string, fileSize int64) (start, end int64, err error) { if rangeHeader == "" { return 0, 0, fmt.Errorf("empty range header") } if !strings.HasPrefix(rangeHeader, "bytes=") { return 0, 0, fmt.Errorf("invalid range unit: %s", rangeHeader) } rangeSpec := strings.TrimPrefix(rangeHeader, "bytes=") if strings.Contains(rangeSpec, ",") { return 0, 0, fmt.Errorf("multi-part ranges are not supported") } rangeSpec = strings.TrimSpace(rangeSpec) parts := strings.Split(rangeSpec, "-") if len(parts) != 2 { return 0, 0, fmt.Errorf("invalid range format: %s", rangeSpec) } if parts[0] == "" { suffix, parseErr := strconv.ParseInt(parts[1], 10, 64) if parseErr != nil { return 0, 0, fmt.Errorf("invalid suffix range: %s", parts[1]) } if suffix <= 0 || suffix > fileSize { return 0, 0, fmt.Errorf("suffix range %d exceeds file size %d", suffix, fileSize) } start = fileSize - suffix end = fileSize - 1 } else if parts[1] == "" { start, err = strconv.ParseInt(parts[0], 10, 64) if err != nil { return 0, 0, fmt.Errorf("invalid range start: %s", parts[0]) } if start >= fileSize { return 0, 0, fmt.Errorf("range start %d exceeds file size %d", start, fileSize) } end = fileSize - 1 } else { start, err = strconv.ParseInt(parts[0], 10, 64) if err != nil { return 0, 0, fmt.Errorf("invalid range start: %s", parts[0]) } end, err = strconv.ParseInt(parts[1], 10, 64) if err != nil { return 0, 0, fmt.Errorf("invalid range end: %s", parts[1]) } if start > end { return 0, 0, fmt.Errorf("range start %d > end %d", start, end) } if start >= fileSize { return 0, 0, fmt.Errorf("range start %d exceeds file size %d", start, fileSize) } if end >= fileSize { end = fileSize - 1 } } return start, end, nil } // StreamFile sends a full file as an HTTP response with proper headers. func StreamFile(c *gin.Context, reader io.ReadCloser, filename string, fileSize int64, contentType string) { defer reader.Close() c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) c.Header("Content-Type", contentType) c.Header("Content-Length", strconv.FormatInt(fileSize, 10)) c.Header("Accept-Ranges", "bytes") c.Status(http.StatusOK) io.Copy(c.Writer, reader) } // StreamRange sends a partial content response (206) for a byte range. func StreamRange(c *gin.Context, reader io.ReadCloser, start, end, totalSize int64, contentType string) { defer reader.Close() contentLength := end - start + 1 c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize)) c.Header("Content-Type", contentType) c.Header("Content-Length", strconv.FormatInt(contentLength, 10)) c.Header("Accept-Ranges", "bytes") c.Status(http.StatusPartialContent) io.Copy(c.Writer, reader) }