213 lines
5.4 KiB
Go
213 lines
5.4 KiB
Go
package storage
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"mime"
|
||
"net/http"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
commonhttp "git.toowon.com/jimmy/go-commom/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) {
|
||
if r.Method != http.MethodPost {
|
||
commonhttp.NotFound(w, "Method not allowed")
|
||
return
|
||
}
|
||
|
||
// 解析multipart表单
|
||
err := r.ParseMultipartForm(h.maxFileSize)
|
||
if err != nil {
|
||
commonhttp.BadRequest(w, fmt.Sprintf("Failed to parse form: %v", err))
|
||
return
|
||
}
|
||
|
||
// 获取文件
|
||
file, header, err := r.FormFile("file")
|
||
if err != nil {
|
||
commonhttp.BadRequest(w, fmt.Sprintf("Failed to get file: %v", err))
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
// 检查文件大小
|
||
if h.maxFileSize > 0 && header.Size > h.maxFileSize {
|
||
commonhttp.Error(w, 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 {
|
||
commonhttp.Error(w, 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 {
|
||
commonhttp.SystemError(w, fmt.Sprintf("Failed to upload file: %v", err))
|
||
return
|
||
}
|
||
|
||
// 获取文件URL
|
||
fileURL, err := h.storage.GetURL(objectKey, 0)
|
||
if err != nil {
|
||
commonhttp.SystemError(w, fmt.Sprintf("Failed to get file URL: %v", err))
|
||
return
|
||
}
|
||
|
||
// 返回结果
|
||
result := UploadResult{
|
||
ObjectKey: objectKey,
|
||
URL: fileURL,
|
||
Size: header.Size,
|
||
ContentType: contentType,
|
||
UploadTime: time.Now(),
|
||
}
|
||
|
||
commonhttp.SuccessWithMessage(w, "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) {
|
||
if r.Method != http.MethodGet {
|
||
commonhttp.NotFound(w, "Method not allowed")
|
||
return
|
||
}
|
||
|
||
// 获取对象键
|
||
objectKey := r.URL.Query().Get("key")
|
||
if objectKey == "" {
|
||
commonhttp.BadRequest(w, "Missing parameter: key")
|
||
return
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
ctx := r.Context()
|
||
exists, err := h.storage.Exists(ctx, objectKey)
|
||
if err != nil {
|
||
commonhttp.SystemError(w, fmt.Sprintf("Failed to check file existence: %v", err))
|
||
return
|
||
}
|
||
|
||
if !exists {
|
||
commonhttp.NotFound(w, "File not found")
|
||
return
|
||
}
|
||
|
||
// 获取文件内容
|
||
reader, err := h.storage.GetObject(ctx, objectKey)
|
||
if err != nil {
|
||
commonhttp.SystemError(w, 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 {
|
||
commonhttp.SystemError(w, fmt.Sprintf("Failed to write response: %v", err))
|
||
return
|
||
}
|
||
}
|