Compare commits
No commits in common. "58a94dcd8e2a68bc92fb384325503f7daf7d5ec6" and "05cb8715f96e5a2aab2eb0e1cfedbbbfe6db024e" have entirely different histories.
58a94dcd8e
...
05cb8715f9
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Yimaru Backend is the server-side application that powers the Yimaru online learning system. It manages courses, lessons, quizzes, student progress, instructor content, and administrative operations for institutions and users on the platform.
|
Yimaru Backend is the server-side application that powers the Yimaru online learning system. It manages courses, lessons, quizzes, student progress, instructor content, and administrative operations for institutions and users on the platform.
|
||||||
|
|
||||||
## Table of Content
|
## Table of Contents
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Environment Configuration](#environment-configuration)
|
- [Environment Configuration](#environment-configuration)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -24,13 +23,6 @@ type uploadMediaRes struct {
|
||||||
EmbedURL string `json:"embed_url,omitempty"`
|
EmbedURL string `json:"embed_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type uploadMediaByURLReq struct {
|
|
||||||
MediaType string `json:"media_type"`
|
|
||||||
SourceURL string `json:"source_url"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveFileURL converts a stored file path to a usable URL.
|
// resolveFileURL converts a stored file path to a usable URL.
|
||||||
// If the path starts with "minio://", it generates a presigned URL.
|
// If the path starts with "minio://", it generates a presigned URL.
|
||||||
// Otherwise it returns the path as-is (e.g. "/static/...").
|
// Otherwise it returns the path as-is (e.g. "/static/...").
|
||||||
|
|
@ -92,24 +84,6 @@ func (h *Handler) GetFileURL(c *fiber.Ctx) error {
|
||||||
// @Router /api/v1/files/upload [post]
|
// @Router /api/v1/files/upload [post]
|
||||||
func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
||||||
mediaType := strings.ToLower(strings.TrimSpace(c.FormValue("media_type")))
|
mediaType := strings.ToLower(strings.TrimSpace(c.FormValue("media_type")))
|
||||||
sourceURL := strings.TrimSpace(c.FormValue("source_url"))
|
|
||||||
title := strings.TrimSpace(c.FormValue("title"))
|
|
||||||
description := strings.TrimSpace(c.FormValue("description"))
|
|
||||||
|
|
||||||
if strings.Contains(strings.ToLower(c.Get("Content-Type")), "application/json") {
|
|
||||||
var req uploadMediaByURLReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
mediaType = strings.ToLower(strings.TrimSpace(req.MediaType))
|
|
||||||
sourceURL = strings.TrimSpace(req.SourceURL)
|
|
||||||
title = strings.TrimSpace(req.Title)
|
|
||||||
description = strings.TrimSpace(req.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mediaType == "" {
|
if mediaType == "" {
|
||||||
mediaType = "file"
|
mediaType = "file"
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +94,14 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "File is required",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
maxSize := int64(100 * 1024 * 1024) // default 100MB
|
maxSize := int64(100 * 1024 * 1024) // default 100MB
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case "image":
|
case "image":
|
||||||
|
|
@ -129,6 +111,13 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
||||||
case "video":
|
case "video":
|
||||||
maxSize = 500 * 1024 * 1024
|
maxSize = 500 * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
if fileHeader.Size > maxSize {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "File too large",
|
||||||
|
Error: "File exceeds size limit for selected media_type",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if mediaType == "video" && h.vimeoSvc == nil {
|
if mediaType == "video" && h.vimeoSvc == nil {
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
|
||||||
Message: "Vimeo service is not available for video uploads",
|
Message: "Vimeo service is not available for video uploads",
|
||||||
|
|
@ -140,73 +129,85 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
fh, err := fileHeader.Open()
|
||||||
fileName string
|
|
||||||
contentType string
|
|
||||||
data []byte
|
|
||||||
)
|
|
||||||
if sourceURL != "" {
|
|
||||||
fetched, err := fetchMediaFromSourceURL(c.Context(), sourceURL, mediaType, maxSize)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to fetch source URL",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fileName = fetched.FileName
|
|
||||||
contentType = fetched.ContentType
|
|
||||||
data = fetched.Data
|
|
||||||
} else {
|
|
||||||
fileHeader, err := c.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "File is required",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if fileHeader.Size > maxSize {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "File too large",
|
|
||||||
Error: "File exceeds size limit for selected media_type",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fh, err := fileHeader.Open()
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to read file",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer fh.Close()
|
|
||||||
|
|
||||||
head := make([]byte, 512)
|
|
||||||
n, _ := fh.Read(head)
|
|
||||||
contentType = http.DetectContentType(head[:n])
|
|
||||||
rest, err := io.ReadAll(fh)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to read file",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
data = append(head[:n], rest...)
|
|
||||||
fileName = fileHeader.Filename
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
contentType, err = normalizeAndValidateMediaContentType(mediaType, contentType, fileName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid file type",
|
Message: "Failed to read file",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
head := make([]byte, 512)
|
||||||
|
n, _ := fh.Read(head)
|
||||||
|
contentType := http.DetectContentType(head[:n])
|
||||||
|
|
||||||
|
switch mediaType {
|
||||||
|
case "image":
|
||||||
|
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid file type",
|
||||||
|
Error: "Only jpg, png, and webp images are allowed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "audio":
|
||||||
|
allowedAudio := map[string]bool{
|
||||||
|
"audio/mpeg": true, "audio/wav": true, "audio/ogg": true, "audio/mp4": true,
|
||||||
|
"audio/aac": true, "audio/webm": true, "video/ogg": true, "video/webm": true,
|
||||||
|
"audio/x-wav": true, "audio/x-m4a": true, "audio/flac": true,
|
||||||
|
}
|
||||||
|
// DetectContentType can return "application/octet-stream" for some files (often via clients
|
||||||
|
// like Postman). If that happens, fall back to extension-based detection.
|
||||||
|
if contentType == "application/octet-stream" {
|
||||||
|
filenameLower := strings.ToLower(fileHeader.Filename)
|
||||||
|
lastDot := strings.LastIndex(filenameLower, ".")
|
||||||
|
ext := ""
|
||||||
|
if lastDot != -1 && lastDot+1 < len(filenameLower) {
|
||||||
|
ext = filenameLower[lastDot+1:]
|
||||||
|
}
|
||||||
|
extMap := map[string]string{
|
||||||
|
"mp3": "audio/mpeg",
|
||||||
|
"wav": "audio/wav",
|
||||||
|
"ogg": "audio/ogg",
|
||||||
|
"m4a": "audio/mp4",
|
||||||
|
"aac": "audio/aac",
|
||||||
|
"webm": "audio/webm",
|
||||||
|
"flac": "audio/flac",
|
||||||
|
}
|
||||||
|
if ct, ok := extMap[ext]; ok {
|
||||||
|
contentType = ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowedAudio[contentType] {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid file type",
|
||||||
|
Error: "Only supported audio formats are allowed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "video":
|
||||||
|
if !strings.HasPrefix(contentType, "video/") {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid file type",
|
||||||
|
Error: "Only video files are allowed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rest, err := io.ReadAll(fh)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to read file",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data := append(head[:n], rest...)
|
||||||
|
|
||||||
if mediaType == "video" {
|
if mediaType == "video" {
|
||||||
|
title := strings.TrimSpace(c.FormValue("title"))
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = fileName
|
title = fileHeader.Filename
|
||||||
}
|
}
|
||||||
|
description := strings.TrimSpace(c.FormValue("description"))
|
||||||
vimeoUpload, err := h.vimeoSvc.UploadVideoFile(c.Context(), title, description, bytes.NewReader(data), int64(len(data)))
|
vimeoUpload, err := h.vimeoSvc.UploadVideoFile(c.Context(), title, description, bytes.NewReader(data), int64(len(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -233,7 +234,7 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
||||||
uploadResult, err := h.minioSvc.Upload(
|
uploadResult, err := h.minioSvc.Upload(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
mediaType,
|
mediaType,
|
||||||
fileName,
|
fileHeader.Filename,
|
||||||
bytes.NewReader(data),
|
bytes.NewReader(data),
|
||||||
int64(len(data)),
|
int64(len(data)),
|
||||||
contentType,
|
contentType,
|
||||||
|
|
@ -260,55 +261,6 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAndValidateMediaContentType(mediaType, contentType, fileName string) (string, error) {
|
|
||||||
contentType = strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
|
|
||||||
if contentType == "" {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mediaType {
|
|
||||||
case "image":
|
|
||||||
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
|
|
||||||
return "", fmt.Errorf("only jpg, png, and webp images are allowed")
|
|
||||||
}
|
|
||||||
case "audio":
|
|
||||||
allowedAudio := map[string]bool{
|
|
||||||
"audio/mpeg": true, "audio/wav": true, "audio/ogg": true, "audio/mp4": true,
|
|
||||||
"audio/aac": true, "audio/webm": true, "video/ogg": true, "video/webm": true,
|
|
||||||
"audio/x-wav": true, "audio/x-m4a": true, "audio/flac": true,
|
|
||||||
}
|
|
||||||
if contentType == "application/octet-stream" {
|
|
||||||
filenameLower := strings.ToLower(fileName)
|
|
||||||
lastDot := strings.LastIndex(filenameLower, ".")
|
|
||||||
ext := ""
|
|
||||||
if lastDot != -1 && lastDot+1 < len(filenameLower) {
|
|
||||||
ext = filenameLower[lastDot+1:]
|
|
||||||
}
|
|
||||||
extMap := map[string]string{
|
|
||||||
"mp3": "audio/mpeg",
|
|
||||||
"wav": "audio/wav",
|
|
||||||
"ogg": "audio/ogg",
|
|
||||||
"m4a": "audio/mp4",
|
|
||||||
"aac": "audio/aac",
|
|
||||||
"webm": "audio/webm",
|
|
||||||
"flac": "audio/flac",
|
|
||||||
}
|
|
||||||
if ct, ok := extMap[ext]; ok {
|
|
||||||
contentType = ct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !allowedAudio[contentType] {
|
|
||||||
return "", fmt.Errorf("only supported audio formats are allowed")
|
|
||||||
}
|
|
||||||
case "video":
|
|
||||||
if !strings.HasPrefix(contentType, "video/") {
|
|
||||||
return "", fmt.Errorf("only video files are allowed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadAudio uploads an audio file to MinIO and returns the object key.
|
// UploadAudio uploads an audio file to MinIO and returns the object key.
|
||||||
// @Summary Upload an audio file
|
// @Summary Upload an audio file
|
||||||
// @Tags files
|
// @Tags files
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
vimeopkg "Yimaru-Backend/internal/pkgs/vimeo"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sourceFetchResult struct {
|
|
||||||
FileName string
|
|
||||||
ContentType string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeSourceURL(raw string, mediaType string) (string, error) {
|
|
||||||
u, err := url.Parse(strings.TrimSpace(raw))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid source URL: %w", err)
|
|
||||||
}
|
|
||||||
if u.Scheme != "http" && u.Scheme != "https" {
|
|
||||||
return "", fmt.Errorf("source URL must use http or https")
|
|
||||||
}
|
|
||||||
|
|
||||||
host := strings.ToLower(u.Hostname())
|
|
||||||
|
|
||||||
if strings.Contains(host, "drive.google.com") {
|
|
||||||
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
|
||||||
// Convert /file/d/{id}/view links to a direct-download endpoint.
|
|
||||||
if len(parts) >= 3 && parts[0] == "file" && parts[1] == "d" {
|
|
||||||
fileID := parts[2]
|
|
||||||
return "https://drive.google.com/uc?export=download&id=" + fileID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mediaType == "video" && (strings.Contains(host, "vimeo.com") || strings.Contains(host, "player.vimeo.com")) {
|
|
||||||
directURL, err := resolveVimeoDirectFileURL(u.String())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if directURL != "" {
|
|
||||||
return directURL, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveVimeoDirectFileURL(vimeoURL string) (string, error) {
|
|
||||||
videoID := vimeopkg.ExtractVideoID(vimeoURL)
|
|
||||||
if videoID == "" {
|
|
||||||
return "", fmt.Errorf("failed to extract Vimeo video id from source URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := url.Parse(vimeoURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid Vimeo source URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configURL := "https://player.vimeo.com/video/" + videoID + "/config"
|
|
||||||
if hash := src.Query().Get("h"); hash != "" {
|
|
||||||
configURL += "?h=" + url.QueryEscape(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, configURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to build Vimeo config request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("User-Agent", "Yimaru-Backend/1.0")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to fetch Vimeo config: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("Vimeo config fetch failed with status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload struct {
|
|
||||||
Request struct {
|
|
||||||
Files struct {
|
|
||||||
Progressive []struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
MimeType string `json:"mime"`
|
|
||||||
} `json:"progressive"`
|
|
||||||
} `json:"files"`
|
|
||||||
} `json:"request"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to decode Vimeo config payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(payload.Request.Files.Progressive) == 0 {
|
|
||||||
return "", fmt.Errorf("no downloadable Vimeo progressive stream found; provide a Vimeo URL with a valid '?h=' hash or a direct file URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(payload.Request.Files.Progressive, func(i, j int) bool {
|
|
||||||
return payload.Request.Files.Progressive[i].Height > payload.Request.Files.Progressive[j].Height
|
|
||||||
})
|
|
||||||
|
|
||||||
return payload.Request.Files.Progressive[0].URL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchMediaFromSourceURL(ctx context.Context, rawSourceURL string, mediaType string, maxSize int64) (*sourceFetchResult, error) {
|
|
||||||
sourceURL, err := normalizeSourceURL(rawSourceURL, mediaType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create source request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", "Yimaru-Backend/1.0")
|
|
||||||
req.Header.Set("Accept", "*/*")
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 2 * time.Minute,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to fetch source URL: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
||||||
return nil, fmt.Errorf("source URL returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
limited := io.LimitReader(resp.Body, maxSize+1)
|
|
||||||
data, err := io.ReadAll(limited)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read source content: %w", err)
|
|
||||||
}
|
|
||||||
if int64(len(data)) > maxSize {
|
|
||||||
return nil, fmt.Errorf("source file exceeds size limit for media type '%s'", mediaType)
|
|
||||||
}
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil, fmt.Errorf("source URL returned empty content")
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
|
||||||
if contentType != "" {
|
|
||||||
contentType = strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
|
|
||||||
}
|
|
||||||
if contentType == "" || contentType == "application/octet-stream" {
|
|
||||||
sampleLen := 512
|
|
||||||
if len(data) < sampleLen {
|
|
||||||
sampleLen = len(data)
|
|
||||||
}
|
|
||||||
contentType = http.DetectContentType(data[:sampleLen])
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := "source-upload"
|
|
||||||
if cd := resp.Header.Get("Content-Disposition"); cd != "" {
|
|
||||||
if _, params, parseErr := mime.ParseMediaType(cd); parseErr == nil {
|
|
||||||
if fn := strings.TrimSpace(params["filename"]); fn != "" {
|
|
||||||
fileName = fn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fileName == "source-upload" {
|
|
||||||
if parsedURL, parseErr := url.Parse(sourceURL); parseErr == nil {
|
|
||||||
base := path.Base(parsedURL.Path)
|
|
||||||
if base != "" && base != "." && base != "/" {
|
|
||||||
fileName = base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &sourceFetchResult{
|
|
||||||
FileName: fileName,
|
|
||||||
ContentType: contentType,
|
|
||||||
Data: data,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user