diff --git a/cmd/server/main.go b/cmd/server/main.go index a2e0f4b..ca661fe 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -28,12 +28,22 @@ func main() { cfg.APIKey, ) + uploadHandler := handler.NewUploadHandler( + cfg.R2AccessKey, + cfg.R2SecretKey, + cfg.R2Bucket, + cfg.R2Endpoint, + cfg.R2CustomDomain, + ) + // Setup Gin router r := gin.Default() // Register routes r.POST("/ocr", ocrHandler.HandleOCR) r.POST("/rate", rateHandler.HandleRate) + // upload file to server + r.POST("/upload", uploadHandler.HandleUpload) // Start server if err := r.Run("localhost:8080"); err != nil { diff --git a/go.mod b/go.mod index 02aea84..5f6188b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module tencenthw go 1.23.4 require ( + github.com/aws/aws-sdk-go v1.55.5 github.com/gin-gonic/gin v1.10.0 github.com/google/generative-ai-go v0.19.0 github.com/joho/godotenv v1.5.1 @@ -35,6 +36,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index 1cc14d2..21203b5 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -59,6 +61,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -145,6 +151,8 @@ google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/g google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 29cf025..8c6cf5f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,6 +10,11 @@ type Config struct { TencentSecretKey string GeminiAPIKey string APIKey string + R2AccessKey string + R2SecretKey string + R2Bucket string + R2Endpoint string + R2CustomDomain string } func LoadConfig() (*Config, error) { @@ -22,5 +27,10 @@ func LoadConfig() (*Config, error) { TencentSecretKey: os.Getenv("TENCENT_SECRET_KEY"), GeminiAPIKey: os.Getenv("GEMINI_API_KEY"), APIKey: os.Getenv("API_KEY"), + R2AccessKey: os.Getenv("R2_ACCESS_KEY"), + R2SecretKey: os.Getenv("R2_SECRET_KEY"), + R2Bucket: os.Getenv("R2_BUCKET"), + R2Endpoint: os.Getenv("R2_ENDPOINT"), + R2CustomDomain: os.Getenv("R2_CUSTOM_DOMAIN"), }, nil } \ No newline at end of file diff --git a/pkg/handler/upload.go b/pkg/handler/upload.go new file mode 100644 index 0000000..7f6ac4e --- /dev/null +++ b/pkg/handler/upload.go @@ -0,0 +1,130 @@ +// 上传文件到cloudflare R2 +package handler +import ( + "bytes" + "fmt" + "net/http" + "github.com/gin-gonic/gin" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" +) + +type UploadHandler struct { + accessKey string + secretKey string + bucket string + endpoint string + customDomain string +} + +type UploadRequest struct { + File string `json:"file" binding:"required"` + APIKey string `json:"apikey" binding:"required"` +} + +type UploadResponse struct { + ImageURL string `json:"image_url"` + Success bool `json:"success"` +} + +func NewUploadHandler(accessKey, secretKey, bucket, endpoint, customDomain string) *UploadHandler { + return &UploadHandler{ + accessKey: accessKey, + secretKey: secretKey, + bucket: bucket, + endpoint: endpoint, + customDomain: customDomain, + } +} +// 上传文件到cloudflare R2。判断文件是否是图片,如果是图片,则上传到R2,并返回图片的url,如果不是图片,则返回错误。 +// 图片大小限制为10M,图片格式为jpg, jpeg, png, gif, bmp, tiff, webp +// HandleUpload 上传文件到Cloudflare R2 +func (h *UploadHandler) HandleUpload(c *gin.Context) { + // 解析请求体 + file, header, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read file from request"}) + return + } + defer file.Close() + + // 读取文件内容 + fileBuffer := make([]byte, header.Size) + _, err = file.Read(fileBuffer) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file content"}) + return + } + + // 验证文件类型 + contentType := http.DetectContentType(fileBuffer) + if !isImage(contentType) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file type. Only images are allowed"}) + return + } + + // 验证文件大小 + if header.Size > 10<<20 { // 10MB + c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds the limit of 10MB"}) + return + } + + // 上传文件到R2 + imageURL, err := h.uploadToR2(fileBuffer, header.Filename, contentType) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to upload file to R2: %v", err)}) + return + } + + // 返回结果 + response := UploadResponse{ + ImageURL: imageURL, + Success: true, + } + c.JSON(http.StatusOK, response) +} + +// uploadToR2 上传文件到Cloudflare R2 +func (h *UploadHandler) uploadToR2(file []byte, fileName, contentType string) (string, error) { + // 创建S3会话 + sess, err := session.NewSession(&aws.Config{ + Endpoint: aws.String(h.endpoint), + Region: aws.String("auto"), + Credentials: credentials.NewStaticCredentials(h.accessKey, h.secretKey, ""), + }) + if err != nil { + return "", fmt.Errorf("failed to create S3 session: %v", err) + } + + // 创建S3服务客户端 + svc := s3.New(sess) + + // 上传文件到R2 + _, err = svc.PutObject(&s3.PutObjectInput{ + Bucket: aws.String(h.bucket), + Key: aws.String(fileName), + Body: bytes.NewReader(file), + ContentType: aws.String(contentType), + ACL: aws.String("public-read"), // 设置文件为公开可读 + }) + if err != nil { + return "", fmt.Errorf("failed to upload file to R2: %v", err) + } + + // 生成文件的URL + imageURL := fmt.Sprintf("https://%s/%s", h.customDomain, fileName) + return imageURL, nil +} + +// isImage 检查文件是否是图片 +func isImage(contentType string) bool { + allowedTypes := []string{"image/jpeg", "image/png", "image/gif", "image/bmp", "image/tiff", "image/webp"} + for _, t := range allowedTypes { + if contentType == t { + return true + } + } + return false +} \ No newline at end of file diff --git a/rate b/rate index 8a535e9..543694b 100755 Binary files a/rate and b/rate differ