diff --git a/app/http/controllers/plugins/s3fs/s3fs_controller.go b/app/http/controllers/plugins/s3fs/s3fs_controller.go new file mode 100644 index 00000000..b620f949 --- /dev/null +++ b/app/http/controllers/plugins/s3fs/s3fs_controller.go @@ -0,0 +1,222 @@ +package s3fs + +import ( + "strings" + + "github.com/bytedance/sonic" + "github.com/goravel/framework/contracts/http" + "github.com/goravel/framework/support/carbon" + "github.com/spf13/cast" + + "panel/app/http/controllers" + "panel/app/services" + "panel/pkg/tools" +) + +type S3fsController struct { + setting services.Setting +} + +type s3fs struct { + ID int64 `json:"id"` + Path string `json:"path"` + Bucket string `json:"bucket"` + Url string `json:"url"` +} + +func NewS3fsController() *S3fsController { + return &S3fsController{ + setting: services.NewSettingImpl(), + } +} + +// List 所有 S3fs 挂载 +func (c *S3fsController) List(ctx http.Context) { + if !controllers.Check(ctx, "s3fs") { + return + } + + page := ctx.Request().QueryInt("page", 1) + limit := ctx.Request().QueryInt("limit", 10) + + var s3fsList []s3fs + err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList) + if err != nil { + controllers.Error(ctx, http.StatusBadRequest, "获取 S3fs 挂载失败") + return + } + + startIndex := (page - 1) * limit + endIndex := page * limit + if startIndex > len(s3fsList) { + controllers.Success(ctx, http.Json{ + "total": 0, + "items": []s3fs{}, + }) + return + } + if endIndex > len(s3fsList) { + endIndex = len(s3fsList) + } + pagedS3fsList := s3fsList[startIndex:endIndex] + + controllers.Success(ctx, http.Json{ + "total": len(s3fsList), + "items": pagedS3fsList, + }) +} + +// Add 添加 S3fs 挂载 +func (c *S3fsController) Add(ctx http.Context) { + if !controllers.Check(ctx, "s3fs") { + return + } + + validator, err := ctx.Request().Validate(map[string]string{ + "ak": "required|regex:^[a-zA-Z0-9]*$", + "sk": "required|regex:^[a-zA-Z0-9]*$", + "bucket": "required|regex:^[a-zA-Z0-9_-]*$", + "url": "required|full_url", + "path": "required|regex:^/[a-zA-Z0-9_-]+$", + }) + if err != nil { + controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error()) + return + } + if validator.Fails() { + controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One()) + return + } + + ak := ctx.Request().Input("ak") + sk := ctx.Request().Input("sk") + path := ctx.Request().Input("path") + bucket := ctx.Request().Input("bucket") + url := ctx.Request().Input("url") + + // 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的 + if strings.Contains(url, bucket) { + controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称") + return + } + + // 检查挂载目录是否存在且为空 + if !tools.Exists(path) { + tools.Mkdir(path, 0755) + } + if !tools.Empty(path) { + controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空") + return + } + + var s3fsList []s3fs + err = sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList) + if err != nil { + controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") + return + } + + for _, s := range s3fsList { + if s.Path == path { + controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在") + return + } + } + + id := carbon.Now().TimestampMilli() + password := ak + ":" + sk + tools.WriteFile("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600) + tools.ExecShell(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`) + check := tools.ExecShell("mount -a 2>&1") + if len(check) != 0 { + tools.ExecShell(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`) + controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check) + return + } + check2 := tools.ExecShell("df -h | grep " + path + " 2>&1") + if len(check2) == 0 { + tools.ExecShell(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`) + controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确") + return + } + + s3fsList = append(s3fsList, s3fs{ + ID: id, + Path: path, + Bucket: bucket, + Url: url, + }) + json, err := sonic.MarshalString(s3fsList) + if err != nil { + controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败") + return + } + err = c.setting.Set("s3fs", json) + if err != nil { + controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败") + return + } + + controllers.Success(ctx, nil) +} + +// Delete 删除 S3fs 挂载 +func (c *S3fsController) Delete(ctx http.Context) { + if !controllers.Check(ctx, "s3fs") { + return + } + + id := ctx.Request().Input("id") + if len(id) == 0 { + controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空") + return + } + + var s3fsList []s3fs + err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList) + if err != nil { + controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") + return + } + + var mount s3fs + for _, s := range s3fsList { + if cast.ToString(s.ID) == id { + mount = s + break + } + } + if mount.ID == 0 { + controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在") + return + } + + tools.ExecShell(`fusermount -u '` + mount.Path + `'`) + tools.ExecShell(`umount '` + mount.Path + `'`) + tools.ExecShell(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`) + check := tools.ExecShell("mount -a 2>&1") + if len(check) != 0 { + controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check) + return + } + tools.RemoveFile("/etc/passwd-s3fs-" + cast.ToString(mount.ID)) + + var newS3fsList []s3fs + for _, s := range s3fsList { + if s.ID != mount.ID { + newS3fsList = append(newS3fsList, s) + } + } + json, err := sonic.MarshalString(newS3fsList) + if err != nil { + controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败") + return + } + err = c.setting.Set("s3fs", json) + if err != nil { + controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败") + return + } + + controllers.Success(ctx, nil) +} diff --git a/app/plugins/s3fs/s3fs.go b/app/plugins/s3fs/s3fs.go new file mode 100644 index 00000000..fb6630e2 --- /dev/null +++ b/app/plugins/s3fs/s3fs.go @@ -0,0 +1,14 @@ +package s3fs + +var ( + Name = "S3fs" + Author = "耗子" + Description = "S3fs 通过 FUSE 挂载兼容 S3 标准的存储桶,例如Amazon S3、阿里云OSS、腾讯云COS、七牛云Kodo等。" + Slug = "s3fs" + Version = "1.9" + Requires = []string{} + Excludes = []string{} + Install = `bash /www/panel/scripts/s3fs/install.sh` + Uninstall = `bash /www/panel/scripts/s3fs/uninstall.sh` + Update = `bash /www/panel/scripts/s3fs/install.sh` +) diff --git a/app/services/plugin.go b/app/services/plugin.go index d3aa1206..8a567fb4 100644 --- a/app/services/plugin.go +++ b/app/services/plugin.go @@ -11,6 +11,7 @@ import ( "panel/app/plugins/php74" "panel/app/plugins/php80" "panel/app/plugins/phpmyadmin" + "panel/app/plugins/s3fs" ) // PanelPlugin 插件元数据结构 @@ -127,6 +128,18 @@ func (r *PluginImpl) All() []PanelPlugin { Uninstall: phpmyadmin.Uninstall, Update: phpmyadmin.Update, }) + p = append(p, PanelPlugin{ + Name: s3fs.Name, + Author: s3fs.Author, + Description: s3fs.Description, + Slug: s3fs.Slug, + Version: s3fs.Version, + Requires: s3fs.Requires, + Excludes: s3fs.Excludes, + Install: s3fs.Install, + Uninstall: s3fs.Uninstall, + Update: s3fs.Update, + }) return p } diff --git a/pkg/tools/string.go b/pkg/tools/string.go index 71d33c09..b13f776a 100644 --- a/pkg/tools/string.go +++ b/pkg/tools/string.go @@ -5,36 +5,10 @@ import ( "crypto/rand" "fmt" "io" - "reflect" "strings" "unicode/utf8" ) -// Empty 类似于 PHP 的 empty() 函数 -func Empty(val interface{}) bool { - if val == nil { - return true - } - v := reflect.ValueOf(val) - switch v.Kind() { - case reflect.String, reflect.Array: - return v.Len() == 0 - case reflect.Map, reflect.Slice: - return v.Len() == 0 || v.IsNil() - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Ptr: - return v.IsNil() - } - return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface()) -} - // FirstElement 安全地获取 args[0],避免 panic: runtime error: index out of range func FirstElement(args []string) string { if len(args) > 0 { diff --git a/pkg/tools/string_test.go b/pkg/tools/string_test.go index 3a30ac21..68c360b9 100644 --- a/pkg/tools/string_test.go +++ b/pkg/tools/string_test.go @@ -14,23 +14,6 @@ func TestStringHelperTestSuite(t *testing.T) { suite.Run(t, &StringHelperTestSuite{}) } -func (s *StringHelperTestSuite) TestEmpty() { - s.True(Empty("")) - s.True(Empty(nil)) - s.True(Empty([]string{})) - s.True(Empty(map[string]string{})) - s.True(Empty(0)) - s.True(Empty(0.0)) - s.True(Empty(false)) - - s.False(Empty(" ")) - s.False(Empty([]string{"Panel"})) - s.False(Empty(map[string]string{"Panel": "HaoZi"})) - s.False(Empty(1)) - s.False(Empty(1.0)) - s.False(Empty(true)) -} - func (s *StringHelperTestSuite) TestFirstElement() { s.Equal("HaoZi", FirstElement([]string{"HaoZi"})) } diff --git a/pkg/tools/system.go b/pkg/tools/system.go index 431e49e7..d0c31299 100644 --- a/pkg/tools/system.go +++ b/pkg/tools/system.go @@ -114,3 +114,13 @@ func Exists(path string) bool { _, err := os.Stat(path) return err == nil } + +// Empty 判断路径是否为空 +func Empty(path string) bool { + files, err := os.ReadDir(path) + if err != nil { + return true + } + + return len(files) == 0 +} diff --git a/pkg/tools/system_test.go b/pkg/tools/system_test.go index 07b2a6cc..b18b8dd5 100644 --- a/pkg/tools/system_test.go +++ b/pkg/tools/system_test.go @@ -99,3 +99,8 @@ func (s *SystemHelperTestSuite) TestExists() { s.True(Exists("/tmp")) s.False(Exists("/tmp/123")) } + +func (s *SystemHelperTestSuite) TestEmpty() { + s.True(Empty("/tmp/123")) + s.False(Empty("/tmp")) +} diff --git a/public/panel/views/plugins/s3fs.html b/public/panel/views/plugins/s3fs.html new file mode 100644 index 00000000..a175071e --- /dev/null +++ b/public/panel/views/plugins/s3fs.html @@ -0,0 +1,102 @@ + +S3fs +
+
+
+
+
S3fs 挂载列表
+
+
+ + + + +
+
+
+
+
+ + diff --git a/public/panel/views/plugins/s3fs/add_mount.html b/public/panel/views/plugins/s3fs/add_mount.html new file mode 100644 index 00000000..2b6a8be1 --- /dev/null +++ b/public/panel/views/plugins/s3fs/add_mount.html @@ -0,0 +1,104 @@ + + + diff --git a/routes/plugin.go b/routes/plugin.go index 3dad6839..d3d8e685 100644 --- a/routes/plugin.go +++ b/routes/plugin.go @@ -10,6 +10,7 @@ import ( "panel/app/http/controllers/plugins/php74" "panel/app/http/controllers/plugins/php80" "panel/app/http/controllers/plugins/phpmyadmin" + "panel/app/http/controllers/plugins/s3fs" "panel/app/http/middleware" ) @@ -129,4 +130,10 @@ func Plugin() { route.Get("info", phpMyAdminController.Info) route.Post("port", phpMyAdminController.SetPort) }) + facades.Route().Prefix("api/plugins/s3fs").Middleware(middleware.Jwt()).Group(func(route route.Route) { + s3fsController := s3fs.NewS3fsController() + route.Get("list", s3fsController.List) + route.Post("add", s3fsController.Add) + route.Post("delete", s3fsController.Delete) + }) } diff --git a/scripts/s3fs/install.sh b/scripts/s3fs/install.sh index 090d73eb..f84baeb9 100644 --- a/scripts/s3fs/install.sh +++ b/scripts/s3fs/install.sh @@ -30,4 +30,4 @@ else exit 1 fi -panel writePlugin s3fs +panel writePlugin s3fs 1.9