diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2420bc4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://cloudreve.org/buy.php"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c22df3b..3c67599 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,10 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v2 with: + clean: false submodules: 'recursive' + - run: | + git fetch --prune --unshallow --tags - name: Get dependencies and build run: | diff --git a/assets b/assets index 63ee122..b44dc05 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 63ee122d977284e3ccf20f4c1ed228584e705c4d +Subproject commit b44dc0514520580fc284937400743123bbb2c6d4 diff --git a/middleware/option.go b/middleware/option.go index a4c6fcd..3aaf619 100644 --- a/middleware/option.go +++ b/middleware/option.go @@ -1,10 +1,13 @@ package middleware import ( + "github.com/HFO4/cloudreve/bootstrap" model "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/hashid" "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" + "io/ioutil" ) // HashID 将给定对象的HashID转换为真实ID @@ -38,3 +41,47 @@ func IsFunctionEnabled(key string) gin.HandlerFunc { c.Next() } } + +// InjectSiteInfo 向首页html中插入站点信息 +func InjectSiteInfo() gin.HandlerFunc { + ignoreFunc := func(c *gin.Context) { + c.Next() + } + if bootstrap.StaticFS == nil { + return ignoreFunc + } + + // 读取index.html + file, err := bootstrap.StaticFS.Open("/index.html") + if err != nil { + util.Log().Warning("静态文件[index.html]不存在,可能会影响首页展示") + return ignoreFunc + } + + fileContentBytes, err := ioutil.ReadAll(file) + if err != nil { + util.Log().Warning("静态文件[index.html]读取失败,可能会影响首页展示") + return ignoreFunc + } + fileContent := string(fileContentBytes) + + return func(c *gin.Context) { + if c.Request.URL.Path == "/" || c.Request.URL.Path == "/index.html" { + // 读取、替换站点设置 + options := model.GetSettingByNames("siteName", "siteKeywords", "siteScript", + "pwa_small_icon") + finalHTML := util.Replace(map[string]string{ + "{siteName}": options["siteName"], + "{siteDes}": options["siteDes"], + "{siteScript}": options["siteScript"], + "{pwa_small_icon}": options["pwa_small_icon"], + }, fileContent) + + c.Header("Content-Type", "text/html") + c.String(200, finalHTML) + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/option_test.go b/middleware/option_test.go index 97d8d5e..1a75f61 100644 --- a/middleware/option_test.go +++ b/middleware/option_test.go @@ -1,12 +1,17 @@ package middleware import ( + "errors" + "github.com/HFO4/cloudreve/bootstrap" "github.com/HFO4/cloudreve/pkg/cache" "github.com/HFO4/cloudreve/pkg/hashid" + "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + testMock "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" + "os" "testing" ) @@ -75,3 +80,107 @@ func TestIsFunctionEnabled(t *testing.T) { } } + +type StaticMock struct { + testMock.Mock +} + +func (m StaticMock) Open(name string) (http.File, error) { + args := m.Called(name) + return args.Get(0).(http.File), args.Error(1) +} + +func (m StaticMock) Exists(prefix string, filepath string) bool { + args := m.Called(prefix, filepath) + return args.Bool(0) +} + +func TestInjectSiteInfo(t *testing.T) { + asserts := assert.New(t) + rec := httptest.NewRecorder() + + // 静态资源未加载 + { + TestFunc := InjectSiteInfo() + + c, _ := gin.CreateTestContext(rec) + c.Params = []gin.Param{} + c.Request, _ = http.NewRequest("GET", "/", nil) + TestFunc(c) + asserts.False(c.IsAborted()) + } + + // index.html 不存在 + { + testStatic := &StaticMock{} + bootstrap.StaticFS = testStatic + testStatic.On("Open", "/index.html"). + Return(&os.File{}, errors.New("error")) + TestFunc := InjectSiteInfo() + + c, _ := gin.CreateTestContext(rec) + c.Params = []gin.Param{} + c.Request, _ = http.NewRequest("GET", "/", nil) + TestFunc(c) + asserts.False(c.IsAborted()) + } + + // index.html 读取失败 + { + file, _ := util.CreatNestedFile("tests/index.html") + file.Close() + testStatic := &StaticMock{} + bootstrap.StaticFS = testStatic + testStatic.On("Open", "/index.html"). + Return(file, nil) + TestFunc := InjectSiteInfo() + + c, _ := gin.CreateTestContext(rec) + c.Params = []gin.Param{} + c.Request, _ = http.NewRequest("GET", "/", nil) + TestFunc(c) + asserts.False(c.IsAborted()) + } + + // 成功且命中 + { + file, _ := util.CreatNestedFile("tests/index.html") + defer file.Close() + testStatic := &StaticMock{} + bootstrap.StaticFS = testStatic + testStatic.On("Open", "/index.html"). + Return(file, nil) + TestFunc := InjectSiteInfo() + + c, _ := gin.CreateTestContext(rec) + c.Params = []gin.Param{} + c.Request, _ = http.NewRequest("GET", "/", nil) + + cache.Set("setting_siteName", "cloudreve", 0) + cache.Set("setting_siteKeywords", "cloudreve", 0) + cache.Set("setting_siteScript", "cloudreve", 0) + cache.Set("setting_pwa_small_icon", "cloudreve", 0) + + TestFunc(c) + asserts.True(c.IsAborted()) + } + + // 成功且未命中 + { + file, _ := util.CreatNestedFile("tests/index.html") + defer file.Close() + testStatic := &StaticMock{} + bootstrap.StaticFS = testStatic + testStatic.On("Open", "/index.html"). + Return(file, nil) + TestFunc := InjectSiteInfo() + + c, _ := gin.CreateTestContext(rec) + c.Params = []gin.Param{} + c.Request, _ = http.NewRequest("GET", "/2", nil) + + TestFunc(c) + asserts.False(c.IsAborted()) + } + +} diff --git a/models/migration.go b/models/migration.go index b50041e..85969e4 100644 --- a/models/migration.go +++ b/models/migration.go @@ -82,6 +82,7 @@ func addDefaultSettings() { {Name: "siteKeywords", Value: `网盘,网盘`, Type: "basic"}, {Name: "siteDes", Value: `Cloudreve`, Type: "basic"}, {Name: "siteTitle", Value: `平步云端`, Type: "basic"}, + {Name: "siteScript", Value: ``, Type: "basic"}, {Name: "fromName", Value: `Cloudreve`, Type: "mail"}, {Name: "mail_keepalive", Value: `30`, Type: "mail"}, {Name: "fromAdress", Value: `no-reply@acg.blue`, Type: "mail"}, diff --git a/pkg/filesystem/manage.go b/pkg/filesystem/manage.go index 21542f8..9a5a636 100644 --- a/pkg/filesystem/manage.go +++ b/pkg/filesystem/manage.go @@ -32,7 +32,7 @@ type Object struct { // Rename 重命名对象 func (fs *FileSystem) Rename(ctx context.Context, dir, file []uint, new string) (err error) { // 验证新名字 - if !fs.ValidateLegalName(ctx, new) || !fs.ValidateExtension(ctx, new) { + if !fs.ValidateLegalName(ctx, new) || (len(file) > 0 && !fs.ValidateExtension(ctx, new)) { return ErrIllegalObjectName } diff --git a/pkg/filesystem/manage_test.go b/pkg/filesystem/manage_test.go index 5ef221d..2d795ce 100644 --- a/pkg/filesystem/manage_test.go +++ b/pkg/filesystem/manage_test.go @@ -707,12 +707,39 @@ func TestFileSystem_Rename(t *testing.T) { asserts.Equal(ErrPathNotExist, err) } - // 新名字不合法 + // 新名字是目录,不合法 { err := fs.Rename(ctx, []uint{10}, []uint{}, "ne/w") asserts.Error(err) asserts.Equal(ErrIllegalObjectName, err) } + + // 新名字是文件,不合法 + { + err := fs.Rename(ctx, []uint{}, []uint{10}, "ne/w") + asserts.Error(err) + asserts.Equal(ErrIllegalObjectName, err) + } + + // 新名字是文件,扩展名不合法 + { + fs.User.Policy.OptionsSerialized.FileType = []string{"txt"} + err := fs.Rename(ctx, []uint{}, []uint{10}, "1.jpg") + asserts.Error(err) + asserts.Equal(ErrIllegalObjectName, err) + } + + // 新名字是目录,不应该检测扩展名 + { + fs.User.Policy.OptionsSerialized.FileType = []string{"txt"} + mock.ExpectQuery("SELECT(.+)folders(.+)"). + WithArgs(10, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + err := fs.Rename(ctx, []uint{10}, []uint{}, "new") + asserts.NoError(mock.ExpectationsWereMet()) + asserts.Error(err) + asserts.Equal(ErrPathNotExist, err) + } } func TestFileSystem_SaveTo(t *testing.T) { diff --git a/routers/router.go b/routers/router.go index ad0da4b..083a007 100644 --- a/routers/router.go +++ b/routers/router.go @@ -82,6 +82,7 @@ func InitMasterRouter() *gin.Engine { 静态资源 */ r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/api/"}))) + r.Use(middleware.InjectSiteInfo()) r.Use(static.Serve("/", bootstrap.StaticFS)) r.GET("manifest.json", controllers.Manifest)