From 4344b1d07c613b9585bf726d77258dac288289ca Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 12 Jun 2026 19:48:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/file/handler.go | 1 + backend/internal/modules/file/service.go | 3 +- backend/internal/modules/file/storage.go | 54 ++++++++++++++++++- backend/internal/modules/file/storage_test.go | 13 +++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 backend/internal/modules/file/storage_test.go diff --git a/backend/internal/modules/file/handler.go b/backend/internal/modules/file/handler.go index 449c90d..fd1b2b6 100644 --- a/backend/internal/modules/file/handler.go +++ b/backend/internal/modules/file/handler.go @@ -87,6 +87,7 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) { } func writeFileError(c *gin.Context, err error) { + _ = c.Error(err) switch { case errors.Is(err, ErrDependencyUnavailable): response.ServiceUnavailable(c, "文件存储未连接") diff --git a/backend/internal/modules/file/service.go b/backend/internal/modules/file/service.go index 0feac52..f0a9792 100644 --- a/backend/internal/modules/file/service.go +++ b/backend/internal/modules/file/service.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "io" "mime/multipart" "net/http" @@ -56,7 +57,7 @@ func (s *Service) Upload(req uploadRequest) (*UploadDTO, error) { if err := s.storage.PutObject(req.Context, key, bytes.NewReader(data), int64(len(data)), contentType, map[string]string{ "original-filename": req.Header.Filename, }); err != nil { - return nil, err + return nil, fmt.Errorf("%w: %v", ErrDependencyUnavailable, err) } var thumbnailURL string var mediumURL string diff --git a/backend/internal/modules/file/storage.go b/backend/internal/modules/file/storage.go index 5bf8fab..33f442a 100644 --- a/backend/internal/modules/file/storage.go +++ b/backend/internal/modules/file/storage.go @@ -70,8 +70,18 @@ func (s *Storage) PutObject(ctx context.Context, key string, reader io.Reader, s } _, err := s.client.PutObject(ctx, s.bucket, key, reader, size, minio.PutObjectOptions{ ContentType: contentType, - UserMetadata: metadata, + UserMetadata: sanitizeObjectMetadata(metadata), }) + if isNoSuchBucketError(err) && rewindReader(reader) == nil { + s.markBucketNotReady() + if readyErr := s.ensureBucketReady(ctx); readyErr != nil { + return readyErr + } + _, err = s.client.PutObject(ctx, s.bucket, key, reader, size, minio.PutObjectOptions{ + ContentType: contentType, + UserMetadata: sanitizeObjectMetadata(metadata), + }) + } return err } @@ -120,6 +130,48 @@ func (s *Storage) ensureBucketReady(ctx context.Context) error { return s.ensureBucket(ctx) } +func (s *Storage) markBucketNotReady() { + s.bucketMu.Lock() + defer s.bucketMu.Unlock() + s.bucketReady = false +} + +func isNoSuchBucketError(err error) bool { + if err == nil { + return false + } + errResp := minio.ToErrorResponse(err) + if errResp.Code == "NoSuchBucket" { + return true + } + return strings.Contains(strings.ToLower(err.Error()), "bucket does not exist") +} + +func rewindReader(reader io.Reader) error { + seeker, ok := reader.(io.Seeker) + if !ok { + return fmt.Errorf("reader cannot rewind") + } + _, err := seeker.Seek(0, io.SeekStart) + return err +} + +func sanitizeObjectMetadata(metadata map[string]string) map[string]string { + if len(metadata) == 0 { + return nil + } + sanitized := make(map[string]string, len(metadata)) + for key, value := range metadata { + key = strings.TrimSpace(key) + if key == "" { + continue + } + // MinIO/S3 用户元数据会作为 HTTP header 发送,值统一转义为 ASCII,避免中文文件名导致上传失败。 + sanitized[key] = url.QueryEscape(value) + } + return sanitized +} + func normalizeEndpoint(raw string) (string, bool, error) { parsed, err := url.Parse(raw) if err != nil { diff --git a/backend/internal/modules/file/storage_test.go b/backend/internal/modules/file/storage_test.go new file mode 100644 index 0000000..d23ad2b --- /dev/null +++ b/backend/internal/modules/file/storage_test.go @@ -0,0 +1,13 @@ +package file + +import "testing" + +func TestSanitizeObjectMetadataEscapesNonASCII(t *testing.T) { + got := sanitizeObjectMetadata(map[string]string{ + "original-filename": "游戏ID截图 1.png", + }) + + if got["original-filename"] != "%E6%B8%B8%E6%88%8FID%E6%88%AA%E5%9B%BE+1.png" { + t.Fatalf("metadata filename = %q", got["original-filename"]) + } +}