diff --git a/app/other/apis/file.go b/app/other/apis/file.go index 8cea78b4..fe8f56d2 100644 --- a/app/other/apis/file.go +++ b/app/other/apis/file.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "go-admin/common/file_store" + "go-admin/config" ) type FileResponse struct { @@ -95,7 +96,7 @@ func (e File) baseImg(c *gin.Context, fileResponse FileResponse, urlPrefix strin source, _ := c.GetPostForm("source") if err := thirdUpload(source, fileName, base64File); err != nil { - e.Error(200, errors.New(""), "上传第三方失败") + e.Error(200, err, "上传第三方失败") return fileResponse } @@ -126,7 +127,7 @@ func (e File) multipleFile(c *gin.Context, urlPrefix string) []FileResponse { fileType, _ := utils.GetType(multipartFileName) if err := thirdUpload(source, fileName, multipartFileName); err != nil { - e.Error(500, errors.New(""), "上传第三方失败") + e.Error(500, err, "上传第三方失败") continue } @@ -176,22 +177,36 @@ func (e File) buildFileResponse(filePath, urlPrefix, fileName, fileType string) } } +// thirdUpload copies the file that was already stored locally to the object +// store the request asked for. source "1", and anything unrecognised, keeps the +// local copy only. +// +// Both branches used to construct a zero-value ALiYunOSS and call UpLoad on it, +// which panicked - and the qiniu branch constructed the aliyun client, so +// source=3 never reached qiniu even in principle. func thirdUpload(source string, name string, path string) error { switch source { case "2": - return ossUpload("img/"+name, path) + return upload(file_store.AliYunOSS, config.ExtConfig.FileStore.AliYun, "img/"+name, path) case "3": - return qiniuUpload("img/"+name, path) + return upload(file_store.QiNiuKodo, config.ExtConfig.FileStore.QiNiu, "img/"+name, path) } return nil } -func ossUpload(name string, path string) error { - oss := file_store.ALiYunOSS{} - return oss.UpLoad(name, path) -} - -func qiniuUpload(name string, path string) error { - oss := file_store.ALiYunOSS{} - return oss.UpLoad(name, path) +func upload(driver file_store.DriverType, store config.ObjectStore, name, path string) error { + if !store.Configured() { + return fmt.Errorf("file store %s is not configured; set it under extend.fileStore", driver) + } + oxs := file_store.OXS{ + Endpoint: store.Endpoint, + AccessKeyID: store.AccessKeyID, + AccessKeySecret: store.AccessKeySecret, + BucketName: store.BucketName, + } + client, err := oxs.Setup(driver) + if err != nil { + return err + } + return client.UpLoad(name, path) } diff --git a/app/other/apis/upload_test.go b/app/other/apis/upload_test.go new file mode 100644 index 00000000..9831ff2d --- /dev/null +++ b/app/other/apis/upload_test.go @@ -0,0 +1,38 @@ +package apis + +import ( + "strings" + "testing" + + "go-admin/config" +) + +// Both branches used to construct a zero-value ALiYunOSS and call UpLoad on it, +// which panicked; the qiniu branch built the aliyun client, so source=3 could +// not have reached qiniu even with credentials. Unconfigured now reports which +// store is missing. +func TestThirdUploadReportsAnUnconfiguredStore(t *testing.T) { + previous := config.ExtConfig.FileStore + config.ExtConfig.FileStore = config.FileStore{} + t.Cleanup(func() { config.ExtConfig.FileStore = previous }) + + for source, want := range map[string]string{"2": "AliYunOSS", "3": "QiNiuKodo"} { + err := thirdUpload(source, "x.png", "/tmp/x.png") + if err == nil { + t.Errorf("source=%s: no error from an unconfigured store", source) + continue + } + if !strings.Contains(err.Error(), want) { + t.Errorf("source=%s: error names %q, want it to mention %s", source, err, want) + } + } +} + +// source 1 and anything unrecognised keep the local copy and do nothing else. +func TestThirdUploadIgnoresLocalAndUnknownSources(t *testing.T) { + for _, source := range []string{"", "1", "9"} { + if err := thirdUpload(source, "x.png", "/tmp/x.png"); err != nil { + t.Errorf("source=%q returned %v, want nil", source, err) + } + } +} diff --git a/config/extend.go b/config/extend.go index bd06e031..e8d6daa6 100644 --- a/config/extend.go +++ b/config/extend.go @@ -3,14 +3,39 @@ package config var ExtConfig Extend // Extend 扩展配置 -// extend: -// demo: -// name: demo-name +// +// extend: +// demo: +// name: demo-name +// // 使用方法: config.ExtConfig......即可!! type Extend struct { - AMap AMap // 这里配置对应配置文件的结构即可 + AMap AMap // 这里配置对应配置文件的结构即可 + FileStore FileStore } type AMap struct { Key string } + +// FileStore 对象存储。上传接口的 source 参数决定走哪一家:2 是阿里云,3 是七牛。 +// 没有填的那一家在被请求时返回明确错误,而不是上传到别处或者崩溃。 +// +// common/file_store 里还实现了华为云 OBS,但上传接口没有对应的 source 取值, +// 所以这里也不为它提供配置。 +type FileStore struct { + AliYun ObjectStore + QiNiu ObjectStore +} + +type ObjectStore struct { + Endpoint string + AccessKeyID string + AccessKeySecret string + BucketName string +} + +// Configured reports whether enough was filled in to attempt a connection. +func (o ObjectStore) Configured() bool { + return o.Endpoint != "" && o.AccessKeyID != "" && o.AccessKeySecret != "" && o.BucketName != "" +} diff --git a/config/extend_test.go b/config/extend_test.go new file mode 100644 index 00000000..230174f4 --- /dev/null +++ b/config/extend_test.go @@ -0,0 +1,16 @@ +package config + +import "testing" + +func TestObjectStoreConfigured(t *testing.T) { + if (ObjectStore{}).Configured() { + t.Fatal("empty store reported as configured") + } + full := ObjectStore{Endpoint: "e", AccessKeyID: "a", AccessKeySecret: "s", BucketName: "b"} + if !full.Configured() { + t.Fatal("complete store reported as unconfigured") + } + if (ObjectStore{Endpoint: "e", AccessKeyID: "a"}).Configured() { + t.Fatal("partial store reported as configured") + } +} diff --git a/config/settings.full.yml b/config/settings.full.yml index bcca7e29..a1fdacce 100644 --- a/config/settings.full.yml +++ b/config/settings.full.yml @@ -56,3 +56,17 @@ settings: extend: # 扩展项使用说明 demo: name: data + # fileStore 对象存储。上传接口的 source 参数决定走哪一家: + # source=1 只存本地,source=2 阿里云 OSS,source=3 七牛 Kodo + # 没有填的那一家在被请求时会返回明确错误,不会静默存到别处。 + fileStore: + aliYun: + endpoint: oss-cn-hangzhou.aliyuncs.com + accessKeyId: "" + accessKeySecret: "" + bucketName: "" + qiNiu: + endpoint: "" + accessKeyId: "" + accessKeySecret: "" + bucketName: ""