mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
接口参数调整
This commit is contained in:
@@ -17,6 +17,7 @@ linters:
|
|||||||
# 代码坏味道
|
# 代码坏味道
|
||||||
- dupl # 重复代码
|
- dupl # 重复代码
|
||||||
- mnd # 魔法数字
|
- mnd # 魔法数字
|
||||||
|
- goconst # 不必要的字符串常量
|
||||||
- cyclop # 包/函数复杂度
|
- cyclop # 包/函数复杂度
|
||||||
- nestif # if 嵌套太深
|
- nestif # if 嵌套太深
|
||||||
- maintidx # 维护性指数
|
- maintidx # 维护性指数
|
||||||
|
|||||||
+4
-16
@@ -3618,14 +3618,8 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)",
|
"description": "图片质量 (low, medium, high, origin),默认为 origin",
|
||||||
"name": "compress",
|
"name": "quality",
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "压缩质量等级 (low, medium, high),默认为 high",
|
|
||||||
"name": "level",
|
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -4404,14 +4398,8 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)",
|
"description": "图片质量 (low, medium, high, origin),默认为 origin",
|
||||||
"name": "compress",
|
"name": "quality",
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "压缩质量等级 (low, medium, high),默认为 high",
|
|
||||||
"name": "level",
|
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+4
-16
@@ -3611,14 +3611,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)",
|
"description": "图片质量 (low, medium, high, origin),默认为 origin",
|
||||||
"name": "compress",
|
"name": "quality",
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "压缩质量等级 (low, medium, high),默认为 high",
|
|
||||||
"name": "level",
|
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -4397,14 +4391,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)",
|
"description": "图片质量 (low, medium, high, origin),默认为 origin",
|
||||||
"name": "compress",
|
"name": "quality",
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "压缩质量等级 (low, medium, high),默认为 high",
|
|
||||||
"name": "level",
|
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+4
-12
@@ -3195,13 +3195,9 @@ paths:
|
|||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: 是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)
|
- description: 图片质量 (low, medium, high, origin),默认为 origin
|
||||||
in: query
|
in: query
|
||||||
name: compress
|
name: quality
|
||||||
type: string
|
|
||||||
- description: 压缩质量等级 (low, medium, high),默认为 high
|
|
||||||
in: query
|
|
||||||
name: level
|
|
||||||
type: string
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/octet-stream
|
- application/octet-stream
|
||||||
@@ -3669,13 +3665,9 @@ paths:
|
|||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: 是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)
|
- description: 图片质量 (low, medium, high, origin),默认为 origin
|
||||||
in: query
|
in: query
|
||||||
name: compress
|
name: quality
|
||||||
type: string
|
|
||||||
- description: 压缩质量等级 (low, medium, high),默认为 high
|
|
||||||
in: query
|
|
||||||
name: level
|
|
||||||
type: string
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/octet-stream
|
- application/octet-stream
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle,} from "@/components/ui/sheet"
|
import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle,} from "@/components/ui/sheet"
|
||||||
import {formatFileSize, UploadService} from "@/lib/services/upload/upload.service"
|
import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service"
|
||||||
import type {Upload as UploadRecord} from "@/lib/services/upload/types"
|
import type {Upload as UploadRecord} from "@/lib/services/upload/types"
|
||||||
|
|
||||||
/* ─── 工具函数 ─────────────────────────────────────────── */
|
/* ─── 工具函数 ─────────────────────────────────────────── */
|
||||||
@@ -310,7 +310,7 @@ export function FilesMain() {
|
|||||||
{file.mime_type.startsWith("image/") ? (
|
{file.mime_type.startsWith("image/") ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
src={`/f/${file.id}`}
|
src={getFileUrl(file.id, "low") ?? undefined}
|
||||||
alt={file.file_name}
|
alt={file.file_name}
|
||||||
className="size-full object-cover"
|
className="size-full object-cover"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
@@ -426,7 +426,7 @@ export function FilesMain() {
|
|||||||
{detailTarget.mime_type.startsWith("image/") ? (
|
{detailTarget.mime_type.startsWith("image/") ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
src={`/f/${detailTarget.id}`}
|
src={getFileUrl(detailTarget.id, "low") ?? undefined}
|
||||||
alt={detailTarget.file_name}
|
alt={detailTarget.file_name}
|
||||||
className="max-h-full max-w-full object-contain rounded-lg shadow-sm"
|
className="max-h-full max-w-full object-contain rounded-lg shadow-sm"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ import {BaseService} from '../core/base.service'
|
|||||||
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types'
|
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types'
|
||||||
import type {InternalAxiosRequestConfig} from 'axios'
|
import type {InternalAxiosRequestConfig} from 'axios'
|
||||||
|
|
||||||
|
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据上传ID构造文件访问URL
|
* 根据上传ID构造文件访问URL
|
||||||
* @param id - 上传记录ID
|
* @param id - 上传记录ID
|
||||||
|
* @param quality - 图片质量
|
||||||
* @returns 文件访问URL
|
* @returns 文件访问URL
|
||||||
*/
|
*/
|
||||||
export function getFileUrl(id: string | number | null | undefined): string | null {
|
export function getFileUrl(
|
||||||
|
id: string | number | null | undefined,
|
||||||
|
quality: ImageQuality = 'origin'
|
||||||
|
): string | null {
|
||||||
if (!id) return null
|
if (!id) return null
|
||||||
return `/f/${id}`
|
if (quality === 'origin') return `/f/${id}`
|
||||||
|
return `/f/${id}?quality=${quality}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ import (
|
|||||||
// @Tags upload
|
// @Tags upload
|
||||||
// @Produce octet-stream
|
// @Produce octet-stream
|
||||||
// @Param id path string true "文件 ID"
|
// @Param id path string true "文件 ID"
|
||||||
// @Param compress query string false "是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)"
|
// @Param quality query string false "图片质量 (low, medium, high, origin),默认为 origin"
|
||||||
// @Param level query string false "压缩质量等级 (low, medium, high),默认为 high"
|
|
||||||
// @Success 200 {file} file "成功获取文件内容"
|
// @Success 200 {file} file "成功获取文件内容"
|
||||||
// @Failure 400 {object} util.ResponseAny "文件 ID 格式错误"
|
// @Failure 400 {object} util.ResponseAny "文件 ID 格式错误"
|
||||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||||
@@ -89,22 +88,16 @@ func getUploadRecordByID(c *gin.Context) (*model.Upload, error) {
|
|||||||
|
|
||||||
// ServeUpload 将已存在的文件内容读取并流式响应给客户端,支持本地和 S3/CDN 驱动,并可选支持 WebP 图片压缩与本地缓存。
|
// ServeUpload 将已存在的文件内容读取并流式响应给客户端,支持本地和 S3/CDN 驱动,并可选支持 WebP 图片压缩与本地缓存。
|
||||||
func ServeUpload(c *gin.Context, upload *model.Upload) {
|
func ServeUpload(c *gin.Context, upload *model.Upload) {
|
||||||
compressStr := c.Query("compress")
|
quality := normalizeImageQuality(c.Query("quality"))
|
||||||
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
|
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
|
||||||
|
|
||||||
if compressStr == "" || !isImage {
|
if quality == "origin" || !isImage {
|
||||||
serveOriginal(c, upload)
|
serveOriginal(c, upload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map level parameter to standard options
|
|
||||||
level := strings.ToLower(c.Query("level"))
|
|
||||||
if level != "low" && level != "medium" && level != "high" {
|
|
||||||
level = "high"
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := diskcache.GetGlobalCache()
|
cache := diskcache.GetGlobalCache()
|
||||||
cacheKey := imageCompressionCacheKey(upload, level)
|
cacheKey := imageCompressionCacheKey(upload, quality)
|
||||||
if webpBytes, err := cache.Get(cacheKey); err == nil {
|
if webpBytes, err := cache.Get(cacheKey); err == nil {
|
||||||
c.Data(http.StatusOK, "image/webp", webpBytes)
|
c.Data(http.StatusOK, "image/webp", webpBytes)
|
||||||
return
|
return
|
||||||
@@ -121,7 +114,7 @@ func ServeUpload(c *gin.Context, upload *model.Upload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compress to WebP
|
// Compress to WebP
|
||||||
webpBytes, err := CompressImageToWebP(bytes.NewReader(origBytes), level)
|
webpBytes, err := CompressImageToWebP(bytes.NewReader(origBytes), quality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorF(c.Request.Context(), "failed to compress image to WebP: %v", err)
|
logger.ErrorF(c.Request.Context(), "failed to compress image to WebP: %v", err)
|
||||||
serveOriginal(c, upload)
|
serveOriginal(c, upload)
|
||||||
@@ -136,17 +129,26 @@ func ServeUpload(c *gin.Context, upload *model.Upload) {
|
|||||||
c.Data(http.StatusOK, "image/webp", webpBytes)
|
c.Data(http.StatusOK, "image/webp", webpBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageCompressionCacheKey(upload *model.Upload, level string) string {
|
func imageCompressionCacheKey(upload *model.Upload, quality string) string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"upload_webp_v1_%d_%d_%d_%s_%s",
|
"upload_webp_v1_%d_%d_%d_%s_%s",
|
||||||
upload.ID,
|
upload.ID,
|
||||||
upload.UpdatedAt.UnixNano(),
|
upload.UpdatedAt.UnixNano(),
|
||||||
upload.FileSize,
|
upload.FileSize,
|
||||||
upload.Hash,
|
upload.Hash,
|
||||||
level,
|
quality,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeImageQuality(quality string) string {
|
||||||
|
switch strings.ToLower(quality) {
|
||||||
|
case "low", "medium", "high":
|
||||||
|
return strings.ToLower(quality)
|
||||||
|
default:
|
||||||
|
return "origin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// serveOriginal 原始文件的流式响应逻辑
|
// serveOriginal 原始文件的流式响应逻辑
|
||||||
func serveOriginal(c *gin.Context, upload *model.Upload) {
|
func serveOriginal(c *gin.Context, upload *model.Upload) {
|
||||||
if upload.StorageDriver == "local" || (upload.StorageDriver == "" && !storage.IsEnabled()) {
|
if upload.StorageDriver == "local" || (upload.StorageDriver == "" && !storage.IsEnabled()) {
|
||||||
|
|||||||
@@ -279,8 +279,8 @@ func TestImageCompression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("serve compressed WebP file with compress=true", func(t *testing.T) {
|
t.Run("serve compressed WebP file with medium quality", func(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/f/3001?compress=true&level=medium", nil)
|
req, _ := http.NewRequest("GET", "/f/3001?quality=medium", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
@@ -320,17 +320,43 @@ func TestImageCompression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("serve compressed with default quality high", func(t *testing.T) {
|
t.Run("serve original file with origin quality", func(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/f/3001?compress=true", nil)
|
req, _ := http.NewRequest("GET", "/f/3001?quality=origin", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Fatalf("expected status 200, got %d", w.Code)
|
t.Fatalf("expected status 200, got %d", w.Code)
|
||||||
}
|
}
|
||||||
cacheKey := imageCompressionCacheKey(&uploadRecord, "high")
|
if w.Header().Get("Content-Type") != "image/png" {
|
||||||
if _, err := cache.Get(cacheKey); err != nil {
|
t.Errorf("expected Content-Type image/png, got %s", w.Header().Get("Content-Type"))
|
||||||
t.Errorf("disk cache Get(%q) returned error: %v", cacheKey, err)
|
}
|
||||||
|
if !bytes.Equal(w.Body.Bytes(), pngBuf.Bytes()) {
|
||||||
|
t.Errorf("origin-quality response differs from original image")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeImageQuality(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
quality string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "low", quality: "low", want: "low"},
|
||||||
|
{name: "medium", quality: "medium", want: "medium"},
|
||||||
|
{name: "high", quality: "high", want: "high"},
|
||||||
|
{name: "origin", quality: "origin", want: "origin"},
|
||||||
|
{name: "uppercase", quality: "LOW", want: "low"},
|
||||||
|
{name: "empty", quality: "", want: "origin"},
|
||||||
|
{name: "invalid", quality: "maximum", want: "origin"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := normalizeImageQuality(tt.quality); got != tt.want {
|
||||||
|
t.Errorf("normalizeImageQuality(%q) = %q, want %q", tt.quality, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,8 +174,7 @@ func UploadFile(c *gin.Context) {
|
|||||||
// @Tags upload
|
// @Tags upload
|
||||||
// @Produce octet-stream
|
// @Produce octet-stream
|
||||||
// @Param id path string true "文件 ID"
|
// @Param id path string true "文件 ID"
|
||||||
// @Param compress query string false "是否启用压缩 (传任意非空值代表启用,非图片文件将被忽略)"
|
// @Param quality query string false "图片质量 (low, medium, high, origin),默认为 origin"
|
||||||
// @Param level query string false "压缩质量等级 (low, medium, high),默认为 high"
|
|
||||||
// @Security SessionCookie
|
// @Security SessionCookie
|
||||||
// @Success 200 {file} file "成功下载文件"
|
// @Success 200 {file} file "成功下载文件"
|
||||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||||
@@ -198,10 +197,10 @@ func DownloadFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileName := upload.FileName
|
fileName := upload.FileName
|
||||||
compressStr := c.Query("compress")
|
quality := normalizeImageQuality(c.Query("quality"))
|
||||||
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
|
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
|
||||||
|
|
||||||
if compressStr != "" && isImage {
|
if quality != "origin" && isImage {
|
||||||
ext := filepath.Ext(fileName)
|
ext := filepath.Ext(fileName)
|
||||||
if ext != "" {
|
if ext != "" {
|
||||||
fileName = strings.TrimSuffix(fileName, ext) + ".webp"
|
fileName = strings.TrimSuffix(fileName, ext) + ".webp"
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ func ValidateS3Key(key string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompressImageToWebP decodes an image from srcReader and encodes it into WebP format
|
// CompressImageToWebP decodes an image from srcReader and encodes it into WebP format
|
||||||
// using the specified quality level (low -> 60, medium -> 75, high -> 85).
|
// using the specified quality (low -> 60, medium -> 75, high -> 85).
|
||||||
func CompressImageToWebP(srcReader io.Reader, qualityLevel string) ([]byte, error) {
|
func CompressImageToWebP(srcReader io.Reader, quality string) ([]byte, error) {
|
||||||
// Decode the image
|
// Decode the image
|
||||||
img, format, err := image.Decode(srcReader)
|
img, format, err := image.Decode(srcReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,22 +52,22 @@ func CompressImageToWebP(srcReader io.Reader, qualityLevel string) ([]byte, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine quality
|
// Determine quality
|
||||||
var quality float32
|
var qualityScore float32
|
||||||
switch strings.ToLower(qualityLevel) {
|
switch strings.ToLower(quality) {
|
||||||
case "low":
|
case "low":
|
||||||
quality = 60
|
qualityScore = 60
|
||||||
case "medium":
|
case "medium":
|
||||||
quality = 75
|
qualityScore = 75
|
||||||
case "high", "":
|
case "high", "":
|
||||||
quality = 85
|
qualityScore = 85
|
||||||
default:
|
default:
|
||||||
quality = 85
|
qualityScore = 85
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode to WebP
|
// Encode to WebP
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = webp.Encode(&buf, img, &webp.EncoderOptions{
|
err = webp.Encode(&buf, img, &webp.EncoderOptions{
|
||||||
Quality: quality,
|
Quality: qualityScore,
|
||||||
Method: 4, // Default method
|
Method: 4, // Default method
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user