fix: upload image issues
This commit is contained in:
@@ -2,7 +2,10 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
@@ -41,32 +44,35 @@ func (s *common) Options(ctx context.Context) (*common_dto.OptionsResponse, erro
|
|||||||
func (s *common) Upload(ctx context.Context, userID int64, file *multipart.FileHeader, typeArg string) (*common_dto.UploadResult, error) {
|
func (s *common) Upload(ctx context.Context, userID int64, file *multipart.FileHeader, typeArg string) (*common_dto.UploadResult, error) {
|
||||||
// Mock Upload to S3/MinIO (Here we just generate key, actual upload handling via direct upload or stream is better)
|
// Mock Upload to S3/MinIO (Here we just generate key, actual upload handling via direct upload or stream is better)
|
||||||
// But this Upload endpoint accepts file. So we save it.
|
// But this Upload endpoint accepts file. So we save it.
|
||||||
// We need to use storage provider to save it?
|
|
||||||
// Storage Provider in my implementation only had SignURL/Verify.
|
|
||||||
// It didn't have "PutObject".
|
|
||||||
// But `storage.go` controller has `Upload`.
|
|
||||||
// This `common.Upload` seems to be the "Backend Upload" endpoint implementation.
|
|
||||||
// It receives file stream.
|
|
||||||
// So `common.Upload` should save the file using logic similar to `storage.go` controller?
|
|
||||||
// Or `storage.go` controller uses `common`?
|
|
||||||
// No, `storage.go` controller uses `services.Storage.Verify`.
|
|
||||||
// The `Upload` endpoint in `common.go` is `/v1/upload`. It's a "Simple Upload" (Form Data).
|
|
||||||
// The `storage.go` controller is for Presigned URL (PUT).
|
|
||||||
// For "Simple Upload", I should implement saving to disk here too?
|
|
||||||
// Or delegate?
|
|
||||||
// I'll implement saving to disk here to match "Local Storage" behavior.
|
|
||||||
// BUT, `common` service shouldn't depend on `os` / `filepath` if it's "Cloud Agnostic".
|
|
||||||
// Ideally `Storage` provider has `PutObject(reader)`.
|
|
||||||
// But I implemented `SignURL` only in `Storage` provider.
|
|
||||||
// To support `Upload` here, I should add `PutObject` to `Storage` provider.
|
|
||||||
// But I can't edit provider easily without risking breaking `gen`.
|
|
||||||
// I'll stick to generating Key and Mock URL, OR simple local save.
|
|
||||||
// Since I want "Real Storage" logic (Signed URLs), I should focus on `GetAssetURL`.
|
|
||||||
// For `Upload` here, I'll just save to `LocalPath` (or `./storage`) directly.
|
|
||||||
|
|
||||||
objectKey := uuid.NewString() + "_" + file.Filename
|
objectKey := uuid.NewString() + "_" + file.Filename
|
||||||
|
|
||||||
// TODO: Save file content (omitted for brevity in this step, focusing on URL signing)
|
// Save file content to local storage
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to open uploaded file")
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
localPath := s.storage.Config.LocalPath
|
||||||
|
if localPath == "" {
|
||||||
|
localPath = "./storage" // Fallback
|
||||||
|
}
|
||||||
|
dstPath := filepath.Join(localPath, objectKey)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||||
|
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create storage directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
dst, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create destination file")
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(dst, src); err != nil {
|
||||||
|
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to save file content")
|
||||||
|
}
|
||||||
|
|
||||||
url := s.GetAssetURL(objectKey)
|
url := s.GetAssetURL(objectKey)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Port = 8080
|
|||||||
# Host = "0.0.0.0"
|
# Host = "0.0.0.0"
|
||||||
# 全局路由前缀(可选)
|
# 全局路由前缀(可选)
|
||||||
# BaseURI = "/api/v1"
|
# BaseURI = "/api/v1"
|
||||||
|
|
||||||
[Http.Cors]
|
[Http.Cors]
|
||||||
# dev CORS for Vite dev servers
|
# dev CORS for Vite dev servers
|
||||||
Mode = "dev"
|
Mode = "dev"
|
||||||
@@ -47,6 +46,7 @@ AllowHeaders = "Content-Type,Authorization"
|
|||||||
AllowMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
AllowMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||||
ExposeHeaders = "*"
|
ExposeHeaders = "*"
|
||||||
AllowCredentials = true
|
AllowCredentials = true
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
# =========================
|
# =========================
|
||||||
@@ -126,3 +126,16 @@ Format = "json"
|
|||||||
# Output = "./logs/app.log"
|
# Output = "./logs/app.log"
|
||||||
# 是否启用调用者信息(文件名:行号)
|
# 是否启用调用者信息(文件名:行号)
|
||||||
EnableCaller = true
|
EnableCaller = true
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 存储配置
|
||||||
|
# =========================
|
||||||
|
[Storage]
|
||||||
|
# 存储类型:local | s3
|
||||||
|
Type = "local"
|
||||||
|
# 本地存储路径
|
||||||
|
LocalPath = "./storage"
|
||||||
|
# 签名密钥
|
||||||
|
Secret = "your-storage-secret"
|
||||||
|
# 公共访问URL前缀
|
||||||
|
BaseURL = "http://localhost:8080/v1/storage"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Type string `json:"type" yaml:"type" toml:"type"` // local, s3
|
Type string // local, s3
|
||||||
LocalPath string `json:"local_path" yaml:"local_path" toml:"local_path"` // for local
|
LocalPath string // for local
|
||||||
Secret string `json:"secret" yaml:"secret" toml:"secret"` // for signing
|
Secret string // for signing
|
||||||
BaseURL string `json:"base_url" yaml:"base_url" toml:"base_url"` // public url prefix
|
BaseURL string // public url prefix
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ import (
|
|||||||
"go.ipao.vip/atom/opt"
|
"go.ipao.vip/atom/opt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultPrefix = "Storage"
|
||||||
|
|
||||||
func DefaultProvider() container.ProviderContainer {
|
func DefaultProvider() container.ProviderContainer {
|
||||||
return container.ProviderContainer{
|
return container.ProviderContainer{
|
||||||
Provider: Provide,
|
Provider: Provide,
|
||||||
Options: []opt.Option{},
|
Options: []opt.Option{
|
||||||
|
opt.Prefix(DefaultPrefix),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
backend/storage/.gitignore
vendored
Normal file
2
backend/storage/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
50
backend/tests/storage_test.go
Normal file
50
backend/tests/storage_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"quyun/v2/providers/storage"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStorageProvider(t *testing.T) {
|
||||||
|
Convey("Storage Provider Test", t, func() {
|
||||||
|
// Mock Config to match what we expect in config.toml
|
||||||
|
// We use a map to simulate how unmarshal might see it, or just use the Config struct directly if we can manual init.
|
||||||
|
// But provider uses UnmarshalConfig.
|
||||||
|
|
||||||
|
// To test properly, we should try to boot the provider or check the logic.
|
||||||
|
// Let's manually init the Storage struct with the config we expect to be loaded.
|
||||||
|
|
||||||
|
cfg := &storage.Config{
|
||||||
|
Type: "local",
|
||||||
|
LocalPath: "./storage",
|
||||||
|
Secret: "your-storage-secret",
|
||||||
|
BaseURL: "http://localhost:8080/v1/storage",
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &storage.Storage{Config: cfg}
|
||||||
|
|
||||||
|
Convey("SignURL should return absolute URL with BaseURL", func() {
|
||||||
|
key := "test.png"
|
||||||
|
url, err := s.SignURL("GET", key, 1*time.Hour)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
t.Logf("Generated URL: %s", url)
|
||||||
|
|
||||||
|
So(url, ShouldStartWith, "http://localhost:8080/v1/storage/test.png")
|
||||||
|
So(url, ShouldContainSubstring, "sign=")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to test if config loading actually works (integration test)
|
||||||
|
func TestStorageConfigLoading(t *testing.T) {
|
||||||
|
// This requires setting up the atom container/config loader which is complex in unit test without full boot.
|
||||||
|
// But we can check if `Provide` works with a mock config source if possible.
|
||||||
|
// For now, we trust the logic that if Config is loaded, SignURL works.
|
||||||
|
// The previous issue was likely Config NOT loaded or loaded empty.
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user