feat: stand-alone port s3 server (#6242)

* feat: single port s3 server

* fix: unable to PUT files if not in root dir
pull/5315/head
itsHenry 2024-03-24 15:16:00 +08:00 committed by GitHub
parent 022e0ca292
commit 9c84b6596f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 70 additions and 21 deletions

View File

@ -91,6 +91,27 @@ the address is defined in config file`,
} }
}() }()
} }
s3r := gin.New()
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r)
if conf.Conf.S3.Port != -1 {
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
utils.Log.Infof("start S3 server @ %s", s3Base)
go func() {
var err error
if conf.Conf.S3.SSL {
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
}
if !conf.Conf.S3.SSL {
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpSrv.ListenAndServe()
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
}
}()
}
// Wait for interrupt signal to gracefully shutdown the server with // Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second. // a timeout of 1 second.
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)

View File

@ -180,7 +180,6 @@ func InitialSettings() []model.SettingItem {
{Key: conf.LdapLoginTips, Value: "login with ldap", Type: conf.TypeString, Group: model.LDAP, Flag: model.PUBLIC}, {Key: conf.LdapLoginTips, Value: "login with ldap", Type: conf.TypeString, Group: model.LDAP, Flag: model.PUBLIC},
//s3 settings //s3 settings
{Key: conf.S3Enabled, Value: "false", Type: conf.TypeBool, Group: model.S3, Flag: model.PRIVATE},
{Key: conf.S3AccessKeyId, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE}, {Key: conf.S3AccessKeyId, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
{Key: conf.S3SecretAccessKey, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE}, {Key: conf.S3SecretAccessKey, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
{Key: conf.S3Buckets, Value: "[]", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE}, {Key: conf.S3Buckets, Value: "[]", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},

View File

@ -1,9 +1,10 @@
package conf package conf
import ( import (
"path/filepath"
"github.com/alist-org/alist/v3/cmd/flags" "github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/pkg/utils/random" "github.com/alist-org/alist/v3/pkg/utils/random"
"path/filepath"
) )
type Database struct { type Database struct {
@ -63,6 +64,12 @@ type Cors struct {
AllowHeaders []string `json:"allow_headers" env:"ALLOW_HEADERS"` AllowHeaders []string `json:"allow_headers" env:"ALLOW_HEADERS"`
} }
type S3 struct {
Enable bool `json:"enable" env:"ENABLE"`
Port int `json:"port" env:"PORT"`
SSL bool `json:"ssl" env:"SSL"`
}
type Config struct { type Config struct {
Force bool `json:"force" env:"FORCE"` Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"` SiteURL string `json:"site_url" env:"SITE_URL"`
@ -81,6 +88,7 @@ type Config struct {
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"` TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"` Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
Cors Cors `json:"cors" envPrefix:"CORS_"` Cors Cors `json:"cors" envPrefix:"CORS_"`
S3 S3 `json:"s3" envPrefix:"S3_"`
} }
func DefaultConfig() *Config { func DefaultConfig() *Config {
@ -142,5 +150,10 @@ func DefaultConfig() *Config {
AllowMethods: []string{"*"}, AllowMethods: []string{"*"},
AllowHeaders: []string{"*"}, AllowHeaders: []string{"*"},
}, },
S3: S3{
Enable: false,
Port: 5246,
SSL: false,
},
} }
} }

View File

@ -85,10 +85,9 @@ const (
LdapLoginTips = "ldap_login_tips" LdapLoginTips = "ldap_login_tips"
//s3 //s3
S3Enabled = "s3_enabled" S3Buckets = "s3_buckets"
S3AccessKeyId = "s3_access_key_id" S3AccessKeyId = "s3_access_key_id"
S3SecretAccessKey = "s3_secret_access_key" S3SecretAccessKey = "s3_secret_access_key"
S3Buckets = "s3_buckets"
// qbittorrent // qbittorrent
QbittorrentUrl = "qbittorrent_url" QbittorrentUrl = "qbittorrent_url"

View File

@ -171,3 +171,8 @@ func Cors(r *gin.Engine) {
config.AllowMethods = conf.Conf.Cors.AllowMethods config.AllowMethods = conf.Conf.Cors.AllowMethods
r.Use(cors.New(config)) r.Use(cors.New(config))
} }
func InitS3(e *gin.Engine) {
Cors(e)
S3Server(e.Group("/"))
}

View File

@ -6,20 +6,25 @@ import (
"strings" "strings"
"github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/server/common" "github.com/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/s3" "github.com/alist-org/alist/v3/server/s3"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func S3(g *gin.RouterGroup) { func S3(g *gin.RouterGroup) {
if !setting.GetBool(conf.S3Enabled) { if !conf.Conf.S3.Enable {
g.Any("/*path", func(c *gin.Context) { g.Any("/*path", func(c *gin.Context) {
common.ErrorStrResp(c, "S3 server is not enabled", 403) common.ErrorStrResp(c, "S3 server is not enabled", 403)
}) })
return return
} }
h, _ := s3.NewServer(context.Background(), []string{setting.GetStr(conf.S3AccessKeyId) + "," + setting.GetStr(conf.S3SecretAccessKey)}) if conf.Conf.S3.Port != -1 {
g.Any("/*path", func(c *gin.Context) {
common.ErrorStrResp(c, "S3 server bound to single port", 403)
})
return
}
h, _ := s3.NewServer(context.Background())
g.Any("/*path", func(c *gin.Context) { g.Any("/*path", func(c *gin.Context) {
adjustedPath := strings.TrimPrefix(c.Request.URL.Path, path.Join(conf.URL.Path, "/s3")) adjustedPath := strings.TrimPrefix(c.Request.URL.Path, path.Join(conf.URL.Path, "/s3"))
@ -27,3 +32,14 @@ func S3(g *gin.RouterGroup) {
gin.WrapH(h)(c) gin.WrapH(h)(c)
}) })
} }
func S3Server(g *gin.RouterGroup) {
if !conf.Conf.S3.Enable {
g.Any("/*path", func(c *gin.Context) {
common.ErrorStrResp(c, "S3 server is not enabled", 403)
})
return
}
h, _ := s3.NewServer(context.Background())
g.Any("/*path", gin.WrapH(h))
}

View File

@ -299,7 +299,7 @@ func (b *s3Backend) PutObject(
Mimetype: meta["Content-Type"], Mimetype: meta["Content-Type"],
} }
err = fs.PutDirectly(ctx, path.Dir(reqPath), stream) err = fs.PutDirectly(ctx, reqPath, stream)
if err != nil { if err != nil {
return result, err return result, err
} }

View File

@ -11,7 +11,7 @@ import (
) )
// Make a new S3 Server to serve the remote // Make a new S3 Server to serve the remote
func NewServer(ctx context.Context, authpair []string) (h http.Handler, err error) { func NewServer(ctx context.Context) (h http.Handler, err error) {
var newLogger logger var newLogger logger
faker := gofakes3.New( faker := gofakes3.New(
newBackend(), newBackend(),
@ -19,7 +19,7 @@ func NewServer(ctx context.Context, authpair []string) (h http.Handler, err erro
gofakes3.WithLogger(newLogger), gofakes3.WithLogger(newLogger),
gofakes3.WithRequestID(rand.Uint64()), gofakes3.WithRequestID(rand.Uint64()),
gofakes3.WithoutVersioning(), gofakes3.WithoutVersioning(),
gofakes3.WithV4Auth(authlistResolver(authpair)), gofakes3.WithV4Auth(authlistResolver()),
gofakes3.WithIntegrityCheck(true), // Check Content-MD5 if supplied gofakes3.WithIntegrityCheck(true), // Check Content-MD5 if supplied
) )

View File

@ -5,7 +5,6 @@ package s3
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"github.com/Mikubill/gofakes3" "github.com/Mikubill/gofakes3"
@ -15,7 +14,6 @@ import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op" "github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting" "github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
) )
type Bucket struct { type Bucket struct {
@ -150,15 +148,13 @@ func prefixParser(p *gofakes3.Prefix) (path, remaining string) {
// } // }
// } // }
func authlistResolver(list []string) map[string]string { func authlistResolver() map[string]string {
authList := make(map[string]string) s3accesskeyid := setting.GetStr(conf.S3AccessKeyId)
for _, v := range list { s3secretaccesskey := setting.GetStr(conf.S3SecretAccessKey)
parts := strings.Split(v, ",") if s3accesskeyid == "" && s3secretaccesskey == "" {
if len(parts) != 2 { return nil
utils.Log.Infof(fmt.Sprintf("Ignored: invalid auth pair %s", v))
continue
}
authList[parts[0]] = parts[1]
} }
authList := make(map[string]string)
authList[s3accesskeyid] = s3secretaccesskey
return authList return authList
} }