diff --git a/conf/conf.ini b/conf/conf.ini index 5aac817..46d0ccd 100644 --- a/conf/conf.ini +++ b/conf/conf.ini @@ -2,6 +2,11 @@ Debug = true SessionSecret = 23333 +[Thumbnail] +MaxWidth = 400 +MaxHeight = 300 +FileSuffix = ._thumb + [Database] Type = mysql User = root diff --git a/models/user.go b/models/user.go index e436575..e24ba87 100644 --- a/models/user.go +++ b/models/user.go @@ -2,12 +2,9 @@ package model import ( "crypto/sha1" - "encoding/binary" "encoding/hex" "encoding/json" - "fmt" "github.com/HFO4/cloudreve/pkg/util" - "github.com/duo-labs/webauthn/webauthn" "github.com/jinzhu/gorm" "github.com/pkg/errors" "strings" @@ -59,41 +56,6 @@ type UserOption struct { PreferredTheme string `json:"preferred_theme"` } -func (user User) WebAuthnID() []byte { - bs := make([]byte, 8) - binary.LittleEndian.PutUint64(bs, uint64(user.ID)) - return bs -} - -func (user User) WebAuthnName() string { - return user.Email -} - -func (user User) WebAuthnDisplayName() string { - return user.Nick -} - -func (user User) WebAuthnIcon() string { - return "https://cdn4.buysellads.net/uu/1/46074/1559075156-slack-carbon-red_2x.png" -} - -func (user User) WebAuthnCredentials() []webauthn.Credential { - var res []webauthn.Credential - err := json.Unmarshal([]byte(user.Authn), &res) - if err != nil { - fmt.Println(err) - } - return res -} - -func (user *User) RegisterAuthn(credential *webauthn.Credential) { - res, err := json.Marshal([]webauthn.Credential{*credential}) - if err != nil { - fmt.Println(err) - } - DB.Model(user).UpdateColumn("authn", string(res)) -} - // Root 获取用户的根目录 func (user *User) Root() (*Folder, error) { var folder Folder @@ -157,18 +119,17 @@ func (user *User) GetPolicyID() uint { return user.Group.PolicyList[0] } return 1 - } else { - // 用户指定时,先检查是否为可用策略列表中的值 - if util.ContainsUint(user.Group.PolicyList, user.OptionsSerialized.PreferredPolicy) { - return user.OptionsSerialized.PreferredPolicy - } - // 不可用时,返回第一个 - if len(user.Group.PolicyList) != 0 { - return user.Group.PolicyList[0] - } - return 1 - } + // 用户指定时,先检查是否为可用策略列表中的值 + if util.ContainsUint(user.Group.PolicyList, user.OptionsSerialized.PreferredPolicy) { + return user.OptionsSerialized.PreferredPolicy + } + // 不可用时,返回第一个 + if len(user.Group.PolicyList) != 0 { + return user.Group.PolicyList[0] + } + return 1 + } // GetUserByID 用ID获取用户 diff --git a/models/user_authn.go b/models/user_authn.go new file mode 100644 index 0000000..6408c70 --- /dev/null +++ b/models/user_authn.go @@ -0,0 +1,53 @@ +package model + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "github.com/duo-labs/webauthn/webauthn" +) + +/* + `webauthn.User` 接口的实现 +*/ + +// WebAuthnID 返回用户ID +func (user User) WebAuthnID() []byte { + bs := make([]byte, 8) + binary.LittleEndian.PutUint64(bs, uint64(user.ID)) + return bs +} + +// WebAuthnName 返回用户名 +func (user User) WebAuthnName() string { + return user.Email +} + +// WebAuthnDisplayName 获得用于展示的用户名 +func (user User) WebAuthnDisplayName() string { + return user.Nick +} + +// WebAuthnIcon 获得用户头像 +func (user User) WebAuthnIcon() string { + return "https://cdn4.buysellads.net/uu/1/46074/1559075156-slack-carbon-red_2x.png" +} + +// WebAuthnCredentials 获得已注册的验证器凭证 +func (user User) WebAuthnCredentials() []webauthn.Credential { + var res []webauthn.Credential + err := json.Unmarshal([]byte(user.Authn), &res) + if err != nil { + fmt.Println(err) + } + return res +} + +// RegisterAuthn 添加新的验证器 +func (user *User) RegisterAuthn(credential *webauthn.Credential) { + res, err := json.Marshal([]webauthn.Credential{*credential}) + if err != nil { + fmt.Println(err) + } + DB.Model(user).UpdateColumn("authn", string(res)) +} diff --git a/models/user_authn_test.go b/models/user_authn_test.go new file mode 100644 index 0000000..9293cbc --- /dev/null +++ b/models/user_authn_test.go @@ -0,0 +1,84 @@ +package model + +import ( + "github.com/DATA-DOG/go-sqlmock" + "github.com/duo-labs/webauthn/webauthn" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUser_RegisterAuthn(t *testing.T) { + asserts := assert.New(t) + credential := webauthn.Credential{} + user := User{ + Model: gorm.Model{ID: 1}, + } + + { + mock.ExpectBegin() + mock.ExpectExec("UPDATE(.+)"). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + user.RegisterAuthn(&credential) + asserts.NoError(mock.ExpectationsWereMet()) + } +} + +func TestUser_WebAuthnCredentials(t *testing.T) { + asserts := assert.New(t) + user := User{ + Model: gorm.Model{ID: 1}, + Authn: `[{"ID":"123","PublicKey":"+4sg1vYcjg/+=","AttestationType":"packed","Authenticator":{"AAGUID":"+lg==","SignCount":0,"CloneWarning":false}}]`, + } + { + credentials := user.WebAuthnCredentials() + asserts.Len(credentials, 1) + } +} + +func TestUser_WebAuthnDisplayName(t *testing.T) { + asserts := assert.New(t) + user := User{ + Model: gorm.Model{ID: 1}, + Nick: "123", + } + { + nick := user.WebAuthnDisplayName() + asserts.Equal("123", nick) + } +} + +func TestUser_WebAuthnIcon(t *testing.T) { + asserts := assert.New(t) + user := User{ + Model: gorm.Model{ID: 1}, + } + { + icon := user.WebAuthnIcon() + asserts.NotEmpty(icon) + } +} + +func TestUser_WebAuthnID(t *testing.T) { + asserts := assert.New(t) + user := User{ + Model: gorm.Model{ID: 1}, + } + { + id := user.WebAuthnID() + asserts.Len(id, 8) + } +} + +func TestUser_WebAuthnName(t *testing.T) { + asserts := assert.New(t) + user := User{ + Model: gorm.Model{ID: 1}, + Email: "abslant@foxmail.com", + } + { + name := user.WebAuthnName() + asserts.Equal("abslant@foxmail.com", name) + } +} diff --git a/pkg/authn/auth.go b/pkg/authn/auth.go index 1d1ebff..5b4187a 100644 --- a/pkg/authn/auth.go +++ b/pkg/authn/auth.go @@ -5,11 +5,11 @@ import ( "github.com/duo-labs/webauthn/webauthn" ) -var Authn *webauthn.WebAuthn +var AuthnInstance *webauthn.WebAuthn func Init() { var err error - Authn, err = webauthn.New(&webauthn.Config{ + AuthnInstance, err = webauthn.New(&webauthn.Config{ RPDisplayName: "Duo Labs", // Display Name for your site RPID: "localhost", // Generally the FQDN for your site RPOrigin: "http://localhost:3000", // The origin URL for WebAuthn requests diff --git a/pkg/authn/auth_test.go b/pkg/authn/auth_test.go new file mode 100644 index 0000000..43344e5 --- /dev/null +++ b/pkg/authn/auth_test.go @@ -0,0 +1,15 @@ +package authn + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInit(t *testing.T) { + asserts := assert.New(t) + + asserts.NotPanics(func() { + Init() + }) + asserts.NotNil(AuthnInstance) +} diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index bfbdc25..7a8e2b7 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -3,7 +3,6 @@ package conf import ( "github.com/HFO4/cloudreve/pkg/util" "github.com/go-ini/ini" - "github.com/mojocn/base64Captcha" "gopkg.in/go-playground/validator.v8" ) @@ -45,36 +44,11 @@ type redis struct { DB string } -// RedisConfig Redis服务器配置 -var RedisConfig = &redis{ - Server: "", - Password: "", - DB: "0", -} - -// DatabaseConfig 数据库配置 -var DatabaseConfig = &database{ - Type: "UNSET", -} - -// SystemConfig 系统公用配置 -var SystemConfig = &system{ - Debug: false, -} - -// CaptchaConfig 验证码配置 -var CaptchaConfig = &captcha{ - Height: 60, - Width: 240, - Mode: 3, - ComplexOfNoiseText: base64Captcha.CaptchaComplexLower, - ComplexOfNoiseDot: base64Captcha.CaptchaComplexLower, - IsShowHollowLine: false, - IsShowNoiseDot: false, - IsShowNoiseText: false, - IsShowSlimeLine: false, - IsShowSineLine: false, - CaptchaLen: 6, +// 缩略图 配置 +type thumb struct { + MaxWidth uint + MaxHeight uint + FileSuffix string `validate:"min=1"` } var cfg *ini.File @@ -90,10 +64,11 @@ func Init(path string) { } sections := map[string]interface{}{ - "Database": DatabaseConfig, - "System": SystemConfig, - "Captcha": CaptchaConfig, - "Redis": RedisConfig, + "Database": DatabaseConfig, + "System": SystemConfig, + "Captcha": CaptchaConfig, + "Redis": RedisConfig, + "Thumbnail": ThumbConfig, } for sectionName, sectionStruct := range sections { err = mapSection(sectionName, sectionStruct) diff --git a/pkg/conf/defaults.go b/pkg/conf/defaults.go new file mode 100644 index 0000000..7f58118 --- /dev/null +++ b/pkg/conf/defaults.go @@ -0,0 +1,41 @@ +package conf + +import "github.com/mojocn/base64Captcha" + +// RedisConfig Redis服务器配置 +var RedisConfig = &redis{ + Server: "", + Password: "", + DB: "0", +} + +// DatabaseConfig 数据库配置 +var DatabaseConfig = &database{ + Type: "UNSET", +} + +// SystemConfig 系统公用配置 +var SystemConfig = &system{ + Debug: false, +} + +// CaptchaConfig 验证码配置 +var CaptchaConfig = &captcha{ + Height: 60, + Width: 240, + Mode: 3, + ComplexOfNoiseText: base64Captcha.CaptchaComplexLower, + ComplexOfNoiseDot: base64Captcha.CaptchaComplexLower, + IsShowHollowLine: false, + IsShowNoiseDot: false, + IsShowNoiseText: false, + IsShowSlimeLine: false, + IsShowSineLine: false, + CaptchaLen: 6, +} + +var ThumbConfig = &thumb{ + MaxWidth: 400, + MaxHeight: 300, + FileSuffix: "._thumb", +} diff --git a/pkg/filesystem/image.go b/pkg/filesystem/image.go index 3b1bfbe..35586a2 100644 --- a/pkg/filesystem/image.go +++ b/pkg/filesystem/image.go @@ -4,6 +4,7 @@ import ( "context" "fmt" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/filesystem/response" "github.com/HFO4/cloudreve/pkg/thumb" "github.com/HFO4/cloudreve/pkg/util" @@ -54,7 +55,7 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { image, err := thumb.NewThumbFromFile(source, file.Name) if err != nil { - util.Log().Warning("生成缩略图时无法解析[%s]图像数据:%s", file.SourceName, err) + util.Log().Warning("生成缩略图时无法解析 [%s] 图像数据:%s", file.SourceName, err) return } @@ -64,9 +65,9 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { // 生成缩略图 image.GetThumb(fs.GenerateThumbnailSize(w, h)) // 保存到文件 - err = image.Save(file.SourceName + "._thumb") + err = image.Save(file.SourceName + conf.ThumbConfig.FileSuffix) if err != nil { - util.Log().Warning("无法保存缩略图:%s", file.SourceName, err) + util.Log().Warning("无法保存缩略图:%s", err) return } @@ -75,12 +76,12 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { // 失败时删除缩略图文件 if err != nil { - _, _ = fs.Handler.Delete(newCtx, []string{file.SourceName + "._thumb"}) + _, _ = fs.Handler.Delete(newCtx, []string{file.SourceName + conf.ThumbConfig.FileSuffix}) } } // GenerateThumbnailSize 获取要生成的缩略图的尺寸 -// TODO 从配置文件读取 +// TODO 优先从数据库中获得 func (fs *FileSystem) GenerateThumbnailSize(w, h int) (uint, uint) { - return 400, 300 + return conf.ThumbConfig.MaxWidth, conf.ThumbConfig.MaxHeight } diff --git a/pkg/filesystem/image_test.go b/pkg/filesystem/image_test.go new file mode 100644 index 0000000..9aef93e --- /dev/null +++ b/pkg/filesystem/image_test.go @@ -0,0 +1,156 @@ +package filesystem + +import ( + "context" + "errors" + "fmt" + "github.com/DATA-DOG/go-sqlmock" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/conf" + "github.com/HFO4/cloudreve/pkg/filesystem/response" + "github.com/HFO4/cloudreve/pkg/util" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + testMock "github.com/stretchr/testify/mock" + "image" + "image/jpeg" + "os" + "testing" +) + +func CreateTestImage() *os.File { + file, err := os.Create("TestFileSystem_GenerateThumbnail.jpeg") + alpha := image.NewAlpha(image.Rect(0, 0, 500, 200)) + jpeg.Encode(file, alpha, nil) + if err != nil { + fmt.Println(err) + } + _, _ = file.Seek(0, 0) + return file +} + +func TestFileSystem_GetThumb(t *testing.T) { + asserts := assert.New(t) + fs := FileSystem{ + User: &model.User{ + Model: gorm.Model{ID: 1}, + }, + } + ctx := context.Background() + + // 正常 + { + testHandler := new(FileHeaderMock) + testHandler.On("Thumb", testMock.Anything, "123.jpg").Return(&response.ContentResponse{URL: "123"}, nil) + fs.Handler = testHandler + mock.ExpectQuery("SELECT(.+)"). + WithArgs(10, 1). + WillReturnRows( + sqlmock.NewRows( + []string{"id", "pic_info", "source_name"}). + AddRow(10, "10,10", "123.jpg"), + ) + + res, err := fs.GetThumb(ctx, 10) + asserts.NoError(mock.ExpectationsWereMet()) + testHandler.AssertExpectations(t) + asserts.NoError(err) + asserts.Equal("123", res.URL) + } + + // 文件不存在 + { + + mock.ExpectQuery("SELECT(.+)"). + WithArgs(10, 1). + WillReturnRows( + sqlmock.NewRows( + []string{"id", "pic_info", "source_name"}), + ) + + _, err := fs.GetThumb(ctx, 10) + asserts.NoError(mock.ExpectationsWereMet()) + asserts.Error(err) + } +} + +func TestFileSystem_GenerateThumbnail(t *testing.T) { + asserts := assert.New(t) + fs := FileSystem{ + User: &model.User{ + Model: gorm.Model{ID: 1}, + }, + } + ctx := context.Background() + + // 成功 + { + src := CreateTestImage() + testHandler := new(FileHeaderMock) + testHandler.On("Get", testMock.Anything, "TestFileSystem_GenerateThumbnail.jpeg").Return(src, nil) + fs.Handler = testHandler + + mock.ExpectBegin() + mock.ExpectExec("UPDATE(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + file := &model.File{ + Name: "123.jpg", + SourceName: "TestFileSystem_GenerateThumbnail.jpeg", + } + + fs.GenerateThumbnail(ctx, file) + asserts.NoError(mock.ExpectationsWereMet()) + testHandler.AssertExpectations(t) + asserts.True(util.Exists("TestFileSystem_GenerateThumbnail.jpeg" + conf.ThumbConfig.FileSuffix)) + + } + + // 更新信息失败后删除文件 + { + src := CreateTestImage() + testHandler := new(FileHeaderMock) + testHandler.On("Get", testMock.Anything, "TestFileSystem_GenerateThumbnail.jpeg").Return(src, nil) + testHandler.On("Delete", testMock.Anything, testMock.Anything).Return([]string{}, nil) + fs.Handler = testHandler + + mock.ExpectBegin() + mock.ExpectExec("UPDATE(.+)").WillReturnError(errors.New("error")) + mock.ExpectRollback() + + file := &model.File{ + Name: "123.jpg", + SourceName: "TestFileSystem_GenerateThumbnail.jpeg", + } + + fs.GenerateThumbnail(ctx, file) + asserts.NoError(mock.ExpectationsWereMet()) + testHandler.AssertExpectations(t) + + } + + // 不能生成缩略图 + { + file := &model.File{ + Name: "123.123", + SourceName: "TestFileSystem_GenerateThumbnail.jpeg", + } + + fs.GenerateThumbnail(ctx, file) + asserts.NoError(mock.ExpectationsWereMet()) + } + +} + +func TestFileSystem_GenerateThumbnailSize(t *testing.T) { + asserts := assert.New(t) + fs := FileSystem{ + User: &model.User{ + Model: gorm.Model{ID: 1}, + }, + } + asserts.NotPanics(func() { + _, _ = fs.GenerateThumbnailSize(0, 0) + }) + +} diff --git a/pkg/filesystem/local/handler.go b/pkg/filesystem/local/handler.go index fc10bc9..087da77 100644 --- a/pkg/filesystem/local/handler.go +++ b/pkg/filesystem/local/handler.go @@ -2,6 +2,7 @@ package local import ( "context" + "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/filesystem/response" "github.com/HFO4/cloudreve/pkg/util" "io" @@ -78,7 +79,7 @@ func (handler Handler) Delete(ctx context.Context, files []string) ([]string, er } // 尝试删除文件的缩略图(如果有) - _ = os.Remove(value + "._thumb") + _ = os.Remove(value + conf.ThumbConfig.FileSuffix) } return deleteFailed, retErr @@ -86,7 +87,7 @@ func (handler Handler) Delete(ctx context.Context, files []string) ([]string, er // Thumb 获取文件缩略图 func (handler Handler) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) { - file, err := handler.Get(ctx, path+"._thumb") + file, err := handler.Get(ctx, path+conf.ThumbConfig.FileSuffix) if err != nil { return nil, err } diff --git a/pkg/thumb/image_test.go b/pkg/thumb/image_test.go new file mode 100644 index 0000000..3411aa9 --- /dev/null +++ b/pkg/thumb/image_test.go @@ -0,0 +1,101 @@ +package thumb + +import ( + "fmt" + "github.com/HFO4/cloudreve/pkg/util" + "github.com/stretchr/testify/assert" + "image" + "image/jpeg" + "os" + "testing" +) + +func CreateTestImage() *os.File { + file, err := os.Create("TestNewThumbFromFile.jpeg") + alpha := image.NewAlpha(image.Rect(0, 0, 500, 200)) + jpeg.Encode(file, alpha, nil) + if err != nil { + fmt.Println(err) + } + _, _ = file.Seek(0, 0) + return file +} + +func TestNewThumbFromFile(t *testing.T) { + asserts := assert.New(t) + file := CreateTestImage() + defer file.Close() + + // 无扩展名时 + { + thumb, err := NewThumbFromFile(file, "123") + asserts.Error(err) + asserts.Nil(thumb) + } + + { + thumb, err := NewThumbFromFile(file, "123.jpg") + asserts.NoError(err) + asserts.NotNil(thumb) + } + { + thumb, err := NewThumbFromFile(file, "123.jpeg") + asserts.Error(err) + asserts.Nil(thumb) + } + { + thumb, err := NewThumbFromFile(file, "123.png") + asserts.Error(err) + asserts.Nil(thumb) + } + { + thumb, err := NewThumbFromFile(file, "123.gif") + asserts.Error(err) + asserts.Nil(thumb) + } + { + thumb, err := NewThumbFromFile(file, "123.3211") + asserts.Error(err) + asserts.Nil(thumb) + } +} + +func TestThumb_GetSize(t *testing.T) { + asserts := assert.New(t) + file := CreateTestImage() + defer file.Close() + thumb, err := NewThumbFromFile(file, "123.jpg") + asserts.NoError(err) + + w, h := thumb.GetSize() + asserts.Equal(500, w) + asserts.Equal(200, h) +} + +func TestThumb_GetThumb(t *testing.T) { + asserts := assert.New(t) + file := CreateTestImage() + defer file.Close() + thumb, err := NewThumbFromFile(file, "123.jpg") + asserts.NoError(err) + + asserts.NotPanics(func() { + thumb.GetThumb(10, 10) + }) +} + +func TestThumb_Save(t *testing.T) { + asserts := assert.New(t) + file := CreateTestImage() + defer file.Close() + thumb, err := NewThumbFromFile(file, "123.jpg") + asserts.NoError(err) + + err = thumb.Save("/:noteexist/") + asserts.Error(err) + + err = thumb.Save("TestThumb_Save.png") + asserts.NoError(err) + asserts.True(util.Exists("TestThumb_Save.png")) + +} diff --git a/routers/controllers/user.go b/routers/controllers/user.go index c4a3b15..268b296 100644 --- a/routers/controllers/user.go +++ b/routers/controllers/user.go @@ -20,7 +20,7 @@ func StartLoginAuthn(c *gin.Context) { return } - options, sessionData, err := authn.Authn.BeginLogin(expectedUser) + options, sessionData, err := authn.AuthnInstance.BeginLogin(expectedUser) if err != nil { c.JSON(200, ErrorResponse(err)) return @@ -52,7 +52,7 @@ func FinishLoginAuthn(c *gin.Context) { var sessionData webauthn.SessionData err = json.Unmarshal(sessionDataJSON, &sessionData) - _, err = authn.Authn.FinishLogin(expectedUser, sessionData, c.Request) + _, err = authn.AuthnInstance.FinishLogin(expectedUser, sessionData, c.Request) if err != nil { c.JSON(200, serializer.Err(401, "用户邮箱或密码错误", err)) @@ -68,7 +68,7 @@ func FinishLoginAuthn(c *gin.Context) { // StartRegAuthn 开始注册WebAuthn信息 func StartRegAuthn(c *gin.Context) { currUser := CurrentUser(c) - options, sessionData, err := authn.Authn.BeginRegistration(currUser) + options, sessionData, err := authn.AuthnInstance.BeginRegistration(currUser) if err != nil { c.JSON(200, ErrorResponse(err)) return @@ -94,7 +94,7 @@ func FinishRegAuthn(c *gin.Context) { var sessionData webauthn.SessionData err := json.Unmarshal(sessionDataJSON, &sessionData) - credential, err := authn.Authn.FinishRegistration(currUser, sessionData, c.Request) + credential, err := authn.AuthnInstance.FinishRegistration(currUser, sessionData, c.Request) currUser.RegisterAuthn(credential) if err != nil {