Files
go-common/storage/handler.go

217 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package storage
import (
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"time"
commonhttp "git.toowon.com/jimmy/go-common/http"
)
// UploadHandler 文件上传处理器
type UploadHandler struct {
storage Storage
maxFileSize int64 // 最大文件大小字节0表示不限制
allowedExts []string // 允许的文件扩展名,空表示不限制
objectPrefix string // 对象键前缀
}
// UploadHandlerConfig 上传处理器配置
type UploadHandlerConfig struct {
Storage Storage
MaxFileSize int64 // 最大文件大小字节0表示不限制
AllowedExts []string // 允许的文件扩展名,空表示不限制
ObjectPrefix string // 对象键前缀(如 "images/", "files/"
}
// NewUploadHandler 创建文件上传处理器
func NewUploadHandler(cfg UploadHandlerConfig) *UploadHandler {
return &UploadHandler{
storage: cfg.Storage,
maxFileSize: cfg.MaxFileSize,
allowedExts: cfg.AllowedExts,
objectPrefix: cfg.ObjectPrefix,
}
}
// ServeHTTP 处理文件上传请求
// 请求方式: POST
// 表单字段: file (文件)
// 可选字段: prefix (对象键前缀,会覆盖配置中的前缀)
func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := commonhttp.NewHandler(w, r)
if r.Method != http.MethodPost {
handler.Error(4001, "Method not allowed")
return
}
// 解析multipart表单
err := r.ParseMultipartForm(h.maxFileSize)
if err != nil {
handler.Error(4002, fmt.Sprintf("Failed to parse form: %v", err))
return
}
// 获取文件
file, header, err := r.FormFile("file")
if err != nil {
handler.Error(4003, fmt.Sprintf("Failed to get file: %v", err))
return
}
defer file.Close()
// 检查文件大小
if h.maxFileSize > 0 && header.Size > h.maxFileSize {
handler.Error(1001, fmt.Sprintf("File size exceeds limit: %d bytes", h.maxFileSize))
return
}
// 检查文件扩展名
if len(h.allowedExts) > 0 {
ext := strings.ToLower(filepath.Ext(header.Filename))
allowed := false
for _, allowedExt := range h.allowedExts {
if strings.ToLower(allowedExt) == ext {
allowed = true
break
}
}
if !allowed {
handler.Error(1002, fmt.Sprintf("File extension not allowed. Allowed: %v", h.allowedExts))
return
}
}
// 生成对象键
prefix := h.objectPrefix
if r.FormValue("prefix") != "" {
prefix = r.FormValue("prefix")
}
// 生成唯一文件名
filename := generateUniqueFilename(header.Filename)
objectKey := GenerateObjectKey(prefix, filename)
// 获取文件类型
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = mime.TypeByExtension(filepath.Ext(header.Filename))
if contentType == "" {
contentType = "application/octet-stream"
}
}
// 上传文件
ctx := r.Context()
err = h.storage.Upload(ctx, objectKey, file, contentType)
if err != nil {
handler.SystemError(fmt.Sprintf("Failed to upload file: %v", err))
return
}
// 获取文件URL
fileURL, err := h.storage.GetURL(objectKey, 0)
if err != nil {
handler.SystemError(fmt.Sprintf("Failed to get file URL: %v", err))
return
}
// 返回结果
result := UploadResult{
ObjectKey: objectKey,
URL: fileURL,
Size: header.Size,
ContentType: contentType,
UploadTime: time.Now(),
}
handler.SuccessWithMessage("Upload successful", result)
}
// generateUniqueFilename 生成唯一文件名
func generateUniqueFilename(originalFilename string) string {
ext := filepath.Ext(originalFilename)
name := strings.TrimSuffix(originalFilename, ext)
timestamp := time.Now().UnixNano()
return fmt.Sprintf("%s_%d%s", name, timestamp, ext)
}
// ProxyHandler 文件代理查看处理器
type ProxyHandler struct {
storage Storage
}
// NewProxyHandler 创建文件代理查看处理器
func NewProxyHandler(storage Storage) *ProxyHandler {
return &ProxyHandler{
storage: storage,
}
}
// ServeHTTP 处理文件查看请求
// URL参数: key (对象键)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := commonhttp.NewHandler(w, r)
if r.Method != http.MethodGet {
handler.Error(4001, "Method not allowed")
return
}
// 获取对象键
objectKey := handler.GetQuery("key", "")
if objectKey == "" {
handler.Error(4004, "Missing parameter: key")
return
}
// 检查文件是否存在
ctx := r.Context()
exists, err := h.storage.Exists(ctx, objectKey)
if err != nil {
handler.SystemError(fmt.Sprintf("Failed to check file existence: %v", err))
return
}
if !exists {
handler.Error(4005, "File not found")
return
}
// 获取文件内容
reader, err := h.storage.GetObject(ctx, objectKey)
if err != nil {
handler.SystemError(fmt.Sprintf("Failed to get file: %v", err))
return
}
defer reader.Close()
// 设置响应头
ext := filepath.Ext(objectKey)
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
}
// 如果是图片设置适当的Content-Type
if strings.HasPrefix(contentType, "image/") {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=31536000") // 缓存1年
} else {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filepath.Base(objectKey)))
}
// 复制文件内容到响应
_, err = io.Copy(w, reader)
if err != nil {
handler.SystemError(fmt.Sprintf("Failed to write response: %v", err))
return
}
}