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 (对象键) // 注意:此方法直接返回文件内容(二进制),错误时返回标准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 } }