Files
go-common/storage/handler.go

202 lines
5.1 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 处理文件上传请求
func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := commonhttp.NewHandler(w, r)
if r.Method != http.MethodPost {
handler.Error("common.method_not_allowed")
return
}
err := r.ParseMultipartForm(h.maxFileSize)
if err != nil {
handler.Error("common.invalid_request")
return
}
file, header, err := r.FormFile("file")
if err != nil {
handler.Error("common.invalid_request")
return
}
defer file.Close()
if h.maxFileSize > 0 && header.Size > h.maxFileSize {
handler.Error("storage.file_too_large")
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("storage.invalid_extension")
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()
if err = h.storage.Upload(ctx, objectKey, file, contentType); err != nil {
handler.Error("system.internal_error")
return
}
fileURL, err := h.storage.GetURL(objectKey, 0)
if err != nil {
handler.Error("system.internal_error")
return
}
result := UploadResult{
ObjectKey: objectKey,
URL: fileURL,
Size: header.Size,
ContentType: contentType,
UploadTime: time.Now(),
}
handler.Success(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 (对象键)
// 注意此方法直接返回文件内容二进制错误时返回标准HTTP错误状态码
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取对象键
objectKey := r.URL.Query().Get("key")
if objectKey == "" {
http.Error(w, "Missing parameter: key", http.StatusBadRequest)
return
}
// 检查文件是否存在
ctx := r.Context()
exists, err := h.storage.Exists(ctx, objectKey)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to check file existence: %v", err), http.StatusInternalServerError)
return
}
if !exists {
http.Error(w, "File not found", http.StatusNotFound)
return
}
// 获取文件内容
reader, err := h.storage.GetObject(ctx, objectKey)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get file: %v", err), http.StatusInternalServerError)
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 {
// 如果已经开始写入响应,无法再设置错误状态码
// 这里只能记录错误,无法返回错误响应
return
}
}