202 lines
6.2 KiB
Go
202 lines
6.2 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type OSSConnectionTestResult struct {
|
|
OK bool `json:"ok"`
|
|
Message string `json:"message,omitempty"`
|
|
TestedAt time.Time `json:"testedAt"`
|
|
TestedDigest string `json:"testedDigest"`
|
|
}
|
|
|
|
func (s *Service) TestAdminOSSSetting(actor *model.User, req OSSSettingRequest) (*OSSConnectionTestResult, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
_, current, err := s.readOSSSetting()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.testOSSSetting("platform", "", actor.ID, req, current)
|
|
}
|
|
|
|
func (s *Service) TestUserOSSSetting(actor *model.User, req OSSSettingRequest) (*OSSConnectionTestResult, error) {
|
|
if actor == nil {
|
|
return nil, Unauthorized("请先登录")
|
|
}
|
|
_, platform, err := s.readOSSSetting()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(req.Provider), s3Provider) && !platform.AllowUserS3 {
|
|
return nil, Forbidden("平台管理员尚未允许个人 S3 兼容存储")
|
|
}
|
|
_, current, err := s.readUserOSSSetting(actor.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.testOSSSetting("user", actor.ID, actor.ID, req, current)
|
|
}
|
|
|
|
func (s *Service) testOSSSetting(scope string, ownerID string, actorID string, req OSSSettingRequest, current ossSettingValue) (*OSSConnectionTestResult, error) {
|
|
req.Enabled = true
|
|
value, err := ossSettingFromRequest(req, current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key := scope + ":" + actorID
|
|
s.storageTestMu.Lock()
|
|
if s.activeStorageTests == nil {
|
|
s.activeStorageTests = make(map[string]bool)
|
|
}
|
|
if s.activeStorageTests[key] {
|
|
s.storageTestMu.Unlock()
|
|
return nil, NewAppError(http.StatusConflict, "对象存储连接测试正在进行")
|
|
}
|
|
s.activeStorageTests[key] = true
|
|
s.storageTestMu.Unlock()
|
|
defer func() {
|
|
s.storageTestMu.Lock()
|
|
delete(s.activeStorageTests, key)
|
|
s.storageTestMu.Unlock()
|
|
}()
|
|
|
|
testKey := path.Join(value.PathPrefix, ".yingce-tests", scope, newID())
|
|
payload := []byte("yingce-storage-test")
|
|
if _, err := putOSSObject(value, testKey, "application/octet-stream", int64(len(payload)), bytes.NewReader(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
stream, err := getOSSObjectRange(value, testKey, "bytes=0-3")
|
|
var rangeErr error
|
|
if err != nil {
|
|
rangeErr = err
|
|
} else {
|
|
data, readErr := io.ReadAll(io.LimitReader(stream.body, 5))
|
|
closeErr := stream.body.Close()
|
|
if readErr != nil || closeErr != nil || string(data) != "ying" {
|
|
rangeErr = errors.New("对象存储 Range 读取测试失败")
|
|
}
|
|
}
|
|
if err := deleteOSSObject(value, testKey); err != nil {
|
|
return nil, fmt.Errorf("对象存储删除测试失败:%w", err)
|
|
}
|
|
if rangeErr != nil {
|
|
return nil, rangeErr
|
|
}
|
|
|
|
location, err := s.upsertStorageLocation(scope, ownerID, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
location.TestedDigest = storageTestDigest(value)
|
|
location.TestedAt = &now
|
|
if err := s.repo.SaveStorageLocation(location); err != nil {
|
|
return nil, err
|
|
}
|
|
return &OSSConnectionTestResult{OK: true, Message: "连接测试通过", TestedAt: now, TestedDigest: location.TestedDigest}, nil
|
|
}
|
|
|
|
func (s *Service) upsertStorageLocation(scope string, ownerID string, value ossSettingValue) (*model.StorageLocation, error) {
|
|
digest := storageLocationDigest(value)
|
|
location, err := s.repo.StorageLocationByDigest(scope, ownerID, value.Provider, digest)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
stored, err := s.encryptOSSSettingSecrets(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stored.Enabled = true
|
|
stored.StorageLocationID = ""
|
|
encoded, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if location == nil {
|
|
location = &model.StorageLocation{ID: newID(), Scope: scope, OwnerID: ownerID, Provider: value.Provider, LocationDigest: digest, ValueJSON: string(encoded)}
|
|
if err := s.repo.CreateStorageLocation(location); err != nil {
|
|
return nil, err
|
|
}
|
|
return location, nil
|
|
}
|
|
location.ValueJSON = string(encoded)
|
|
return location, nil
|
|
}
|
|
|
|
func (s *Service) storageLocationValue(id string) (*model.StorageLocation, ossSettingValue, error) {
|
|
location, err := s.repo.StorageLocation(id)
|
|
if err != nil {
|
|
return nil, ossSettingValue{}, err
|
|
}
|
|
var value ossSettingValue
|
|
if err := json.Unmarshal([]byte(location.ValueJSON), &value); err != nil {
|
|
return nil, ossSettingValue{}, errors.New("对象存储位置配置格式无效")
|
|
}
|
|
if _, err := s.decryptOSSSettingSecrets(&value); err != nil {
|
|
return nil, ossSettingValue{}, err
|
|
}
|
|
value.StorageLocationID = location.ID
|
|
return location, normalizeOSSSetting(value), nil
|
|
}
|
|
|
|
func (s *Service) requireTestedS3Location(scope string, ownerID string, value ossSettingValue) (*model.StorageLocation, error) {
|
|
location, err := s.repo.StorageLocationByDigest(scope, ownerID, s3Provider, storageLocationDigest(value))
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, BadAuthRequest("S3 关键配置尚未通过连接测试")
|
|
}
|
|
return nil, err
|
|
}
|
|
if location.TestedAt == nil || location.TestedDigest != storageTestDigest(value) {
|
|
return nil, BadAuthRequest("S3 关键配置或凭据已变化,请重新连接测试")
|
|
}
|
|
return location, nil
|
|
}
|
|
|
|
func (s *Service) populateStorageLocationPublic(result *PublicOSSSetting, scope string, ownerID string, id string) error {
|
|
count, err := s.repo.StorageLocationHistoryCount(scope, ownerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.HistoryCount = count
|
|
if id == "" {
|
|
return nil
|
|
}
|
|
location, err := s.repo.StorageLocation(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.TestedAt = location.TestedAt
|
|
result.TestedDigest = location.TestedDigest
|
|
result.ReferencedResourceCount, err = s.repo.StorageLocationResourceCount(id)
|
|
return err
|
|
}
|
|
|
|
func deleteOSSObject(setting ossSettingValue, objectKey string) error {
|
|
switch normalizeOSSSetting(setting).Provider {
|
|
case s3Provider:
|
|
return deleteS3Object(setting, objectKey)
|
|
case tencentCOSProvider:
|
|
return deleteTencentCOSObject(setting, objectKey)
|
|
case qiniuKodoProvider:
|
|
return deleteQiniuObject(setting, objectKey)
|
|
default:
|
|
return deleteAliyunOSSObject(setting, objectKey)
|
|
}
|
|
}
|