diff --git a/cmd/octopus/main.go b/cmd/octopus/main.go index 9b2733e..00f7850 100644 --- a/cmd/octopus/main.go +++ b/cmd/octopus/main.go @@ -3,9 +3,11 @@ package main import ( "octopus/cmd/octopus/scripts" "octopus/cmd/octopus/server" + "octopus/internal/config" "octopus/internal/dal" "octopus/internal/dal/query" + "github.com/casdoor/casdoor-go-sdk/casdoorsdk" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -19,8 +21,17 @@ var rootCmd = &cobra.Command{ func main() { rootCmd.AddCommand(server.CmdRun) rootCmd.AddCommand(scripts.CmdScripts) - query.SetDefault(dal.GetPostgres()) + db := dal.GetPostgres() + + // migrations.Migrate(db) + + query.SetDefault(db) if err := rootCmd.Execute(); err != nil { log.Fatal().Err(err).Msg("Failed to execute root command") } } + +func init() { + c := config.Get().Casdoor + casdoorsdk.InitConfig(c.Endpoint, c.ClientID, c.ClientSecret, c.Certificate, c.OrganizationName, c.AppName) +} diff --git a/cmd/octopus/server/app.go b/cmd/octopus/server/app.go index 368e5d3..5782f07 100644 --- a/cmd/octopus/server/app.go +++ b/cmd/octopus/server/app.go @@ -1,7 +1,6 @@ package server import ( - "net/http" "time" "octopus" @@ -30,8 +29,10 @@ var innerURLs = tools.NewSet( var ROUTES = []func(app *soda.Soda){ RegisterBase, + router.RegisterAuthRouter, router.RegisterDebuggerRouter, router.RegisterDocRouter, + router.RegisterDocFolderRouters, } // startHttpServer starts configures and starts an HTTP server on the given URL. @@ -52,8 +53,8 @@ func InitApp() *soda.Soda { if err == nil { return c.Next() } - status := http.StatusInternalServerError //default error status - if e, ok := err.(*fiber.Error); ok { // it's a custom error, so use the status in the error + status := 400 //default error status + if e, ok := err.(*fiber.Error); ok { // it's a custom error, so use the status in the error status = e.Code } msg := map[string]interface{}{"code": status, "message": err.Error()} diff --git a/configs/config.yaml b/configs/config.yaml index 1e69101..ce170e8 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -1,7 +1,44 @@ debug: true http_port: 8080 prometheus_port: 18090 +host: http://localhost:8080 databases: - oss: minio://admin:uh8Cz62LKFDe9tBHg2PVyDVX7XKqE584xP@10.16.129.140:31141/pangu2-demo + storage: minio://admin:uh8Cz62LKFDe9tBHg2PVyDVX7XKqE584xP@10.16.129.140:31141/pangu2-demo postgresql: postgres://postgres:TvpHKxDhXzMsVkXWdzwiwUw9KmaLuGdRJo@10.16.129.140:31258/octopus qdrant: 10.16.129.104:32352 +casdoor: + app_name: octopus + endpoint: "http://localhost:8000" + client_id: "ef8e250b7eb21bca7fdc" + client_secret: "eb4ef3828b94a2de425d56db1245c97b8273a4aa" + certificate: | + -----BEGIN CERTIFICATE----- + MIIE3TCCAsWgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMCgxDjAMBgNVBAoTBWFk + bWluMRYwFAYDVQQDEw1jZXJ0LWJ1aWx0LWluMB4XDTIzMDMyMzExMzQ1MloXDTQz + MDMyMzExMzQ1MlowKDEOMAwGA1UEChMFYWRtaW4xFjAUBgNVBAMTDWNlcnQtYnVp + bHQtaW4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsB5WEdp6v6pn+ + Ny0RrGMSsdZlU0pCjxxuYU4kWq2RuF2nBMWWFN9OHuc6NBscmau6UZzeEmYSI202 + b3FKHl1mENF8bcyYDkH4ShvSzlhJsPoMtEVhOFfQnn8wyLRR/HfIrYgQFvQ+2P0f + 1IZ+jzIQL/axmmzkh0rbZ5aait0l115yGyo0zXWSdzqGpec3OJjg2jAWS2U2YdJ4 + gvGWodmNCQ8ytwQNJefySTklfZCD3isPF0ZYxVVPH0I+Ni5XQ2G7EL0gffFLqFhf + iVoU13w1hCrdIRTeJtCOnHoFNolZzqhhXF6/t0o9Fm0f23F4lmoAVtGMZFVY7ghE + sHIpTUf4qnf7J5J3GX9BZ8uhrUYqoTvzydk9yEqPIYgg8801nQKs4NnEBqSpktRP + TzXt+VZwMHquONg2Y59CIqTlw4Uek7IPpPSlI0eR1y7UB38LqH1r03dA3XLnE/mv + FWPiBPcvqaBNDF4TqkSwJv4zDKtTiziSPvdD2w42cPav+IxiOTqQyXWkqNGiIvK0 + WTbWjdWWkqxrJWjfIGZYOC4v/DowjUz9dtKr9m/MKQZhqoxmVzcr/VWJYyh6EPzu + gBtty2QgYRsS6C5vnzY0BPYqOhok9eDYdI7aNO3BOA9CDSa2EDx6RBLDGO3+6XeX + XWwxqMqy1dY5x19DyEn1avfSWARGdwIDAQABoxAwDjAMBgNVHRMBAf8EAjAAMA0G + CSqGSIb3DQEBCwUAA4ICAQCb1yqlbzXawXd/EGiFvSCKK1I7NzsGx/ZwuIc/Xs0R + z1UwJLqdS9DmzRtUGKQNB7PYbThXvt46FLaPaaL1r2fzVvTw4Es4Mym7v12bJEs9 + jsTRoHrSxFTuBHO5QWYj7wW8et9LzUX+VIE02OvSeD6SGxGJFJKfSyjaEnmvJX5f + xYIyI97zXfdFQwIljQu6fhm/jzVF52ysTrBytqOUQ86vxObh27qdkXIXdfetdo4M + X/tIbw5YJCULltUP4BQK2bmrMhy2YpRyijLfa/sC+fuX7s5BABcARkAwSpls3QkQ + 6d1On3jkFdYdN8wRqRVQoeA+LKZ8nrS3znh6/MVvIze4/f8GWIzFRrjPoPrwLdSZ + TtEQmM7I36+79edxD1nVdyZOyp4zleCZfAFdr6twN7PNAjg/qonmxh9kaWD67sfs + ZehotzDeB5aSSeEfwzswJigqt6a4WYVQoV+IaFTsEJWnS/WiLUIHWbXuBueG192B + x+BNtMEvQuG/Y13ukoSpua7vBcO84ZCfuJgVffIHJcRtnaywTF+mBZJLw/BfjWQ/ + /ezEwJRJo0CX4TzB9UhMk9RKjhQAhyWJ9joZGIsVj1T+ldx4cA5IafiUWrOKwbBJ + NKmrQ+oT7U7JhZ6CJZ7qG0sCWb9moj2BrFkZRFPphSbJESefo29sMWe2dfe1Ii9s + fA== + -----END CERTIFICATE----- + organization_name: pangu2 diff --git a/internal/config/config.go b/internal/config/config.go index 12ac28a..f96ebc2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,20 +13,30 @@ import ( "github.com/spf13/viper" ) -type Config struct { - IsLocal bool - Debug bool `mapstructure:"debug"` - HTTPPort int `mapstructure:"http_port" validate:"required"` +type casdoorConfig struct { + Endpoint string `validate:"required" mapstructure:"endpoint"` + ClientID string `validate:"required" mapstructure:"client_id"` + ClientSecret string `validate:"required" mapstructure:"client_secret"` + Certificate string `validate:"required" mapstructure:"certificate"` + OrganizationName string `validate:"required" mapstructure:"organization_name"` + AppName string `validate:"required" mapstructure:"app_name"` +} +type Config struct { + IsLocal bool + Debug bool `mapstructure:"debug"` + HTTPPort int `mapstructure:"http_port" validate:"required"` + Host string `mapstructure:"host" validate:"required"` + Casdoor casdoorConfig `mapstructure:"casdoor"` Databases struct { - OSS string `mapstructure:"oss" validate:"required"` + Storage string `mapstructure:"storage" validate:"required"` PostgreSQL string `mapstructure:"postgresql" validate:"required"` Qdrant string `mapstructure:"qdrant" validate:"required"` } `mapstructure:"databases"` Sentry struct { - Environment string - DSN string - } + Environment string `mapstructure:"environment"` + DSN string `mapstructure:"dsn"` + } `mapstructure:"sentry"` } var ( diff --git a/internal/dal/model/base.go b/internal/dal/model/base.go index 9944288..76951fe 100644 --- a/internal/dal/model/base.go +++ b/internal/dal/model/base.go @@ -3,8 +3,8 @@ package model import "time" type Base struct { - OrgID string `gorm:"column:org_id;type:varchar;primaryKey;comment:组织ID"` // 组织ID - ID string `gorm:"column:id;type:varchar;primaryKey;comment:ID"` // ID + OrgID string `gorm:"column:org_id;type:varchar;primaryKey;comment:组织ID"` + ID string `gorm:"column:id;type:varchar;primaryKey;comment:ID"` CreatedAt time.Time `gorm:"column:created_at;comment:创建时间"` UpdatedAt time.Time `gorm:"column:updated_at;comment:更新时间"` } diff --git a/internal/dal/model/docs.go b/internal/dal/model/docs.go index c46d20e..45a86ff 100644 --- a/internal/dal/model/docs.go +++ b/internal/dal/model/docs.go @@ -5,6 +5,7 @@ import ( "net/url" "octopus/internal/dal" "octopus/internal/schema" + "strings" "time" "github.com/rs/xid" @@ -16,24 +17,29 @@ const TableNameDoc = "docs" type DocFolder struct { Base - Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称 - IsDeletable bool `gorm:"column:is_deletable;type:boolean;not null;default:true"` // 是否允许被删除 - IsEditable bool `gorm:"column:is_editable;type:boolean;not null;default:true"` // 是否允许被修改 - Path string `gorm:"column:path;type:varchar;index:idx_path"` // 路径索引 - CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人 + Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称 + IsDefault bool `gorm:"column:is_default;type:boolean;not null;default:false"` // 是否默认分组 + ParentPath string `gorm:"column:parent_path;type:varchar;index:idx_path"` // 路径索引 + CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人 } func (*DocFolder) TableName() string { return TableNameDocFolder } +func (f *DocFolder) ParentID() string { + if f.ParentPath == "" { + return "" + } + parts := strings.Split(f.ParentPath, "/") + return parts[len(parts)-1] +} func (df *DocFolder) ToSchema() *schema.DocFolder { return &schema.DocFolder{ - ID: df.ID, - IsDeletable: df.IsDeletable, - IsEditable: df.IsEditable, - CreatedAt: df.CreatedAt, - UpdatedAt: df.UpdatedAt, + ID: df.ID, + Name: df.Name, + CreatedAt: df.CreatedAt, + UpdatedAt: df.UpdatedAt, } } @@ -42,6 +48,28 @@ func (d *DocFolder) BeforeCreate(*gorm.DB) error { return nil } +// 在返回树结构的时候有用 +type DocFolderWithChildren struct { + *DocFolder + Children []*DocFolderWithChildren // 子文件夹 +} + +func (df *DocFolderWithChildren) ToTreeSchema() *schema.DocFolderWithChildren { + var traverse func(folder *DocFolderWithChildren) *schema.DocFolderWithChildren + + traverse = func(folder *DocFolderWithChildren) *schema.DocFolderWithChildren { + children := make([]*schema.DocFolderWithChildren, 0, len(folder.Children)) + for _, child := range folder.Children { + children = append(children, traverse(child)) + } + return &schema.DocFolderWithChildren{ + DocFolder: folder.ToSchema(), + Children: children, + } + } + return traverse(df) +} + type Doc struct { Base Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称 @@ -49,7 +77,7 @@ type Doc struct { IsEditable bool `gorm:"column:is_editable;type:boolean;not null;default:true"` // 是否允许编辑名称 FolderID string `gorm:"column:folder_id;type:varchar;index:idx_folder_id"` // 文件夹ID ObjectName string `gorm:"column:object_name;type:varchar"` // 对象存储中对应的object_name - UploadedAt time.Time `gorm:"column:uploaded_at;type:datetime"` // 上传时间 + UploadedAt time.Time `gorm:"column:uploaded_at"` // 上传时间 CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人 Folder *DocFolder `gorm:"foreignKey:FolderID;references:ID"` @@ -62,8 +90,6 @@ func (d *Doc) ToSchema(ctx context.Context) *schema.Doc { ID: d.ID, Folder: d.Folder.ToSchema(), PresignedURL: url, - IsDeletable: d.IsDeletable, - IsEditable: d.IsEditable, UploadedAt: d.UploadedAt, CreatedAt: d.CreatedAt, UpdatedAt: d.UpdatedAt, @@ -77,6 +103,7 @@ func (d *Doc) PresignedURL(ctx context.Context) (*url.URL, error) { func (*Doc) TableName() string { return TableNameDoc } + func (d *Doc) BeforeCreate(*gorm.DB) error { d.ID = xid.New().String() return nil diff --git a/internal/dal/query/doc_folders.gen.go b/internal/dal/query/doc_folders.gen.go index cc5936b..cce151b 100644 --- a/internal/dal/query/doc_folders.gen.go +++ b/internal/dal/query/doc_folders.gen.go @@ -31,9 +31,8 @@ func newDocFolder(db *gorm.DB, opts ...gen.DOOption) docFolder { _docFolder.CreatedAt = field.NewTime(tableName, "created_at") _docFolder.UpdatedAt = field.NewTime(tableName, "updated_at") _docFolder.Name = field.NewString(tableName, "name") - _docFolder.IsDeletable = field.NewBool(tableName, "is_deletable") - _docFolder.IsEditable = field.NewBool(tableName, "is_editable") - _docFolder.Path = field.NewString(tableName, "path") + _docFolder.IsDefault = field.NewBool(tableName, "is_default") + _docFolder.ParentPath = field.NewString(tableName, "parent_path") _docFolder.CreatedBy = field.NewString(tableName, "created_by") _docFolder.fillFieldMap() @@ -44,16 +43,15 @@ func newDocFolder(db *gorm.DB, opts ...gen.DOOption) docFolder { type docFolder struct { docFolderDo - ALL field.Asterisk - OrgID field.String - ID field.String - CreatedAt field.Time - UpdatedAt field.Time - Name field.String - IsDeletable field.Bool - IsEditable field.Bool - Path field.String - CreatedBy field.String + ALL field.Asterisk + OrgID field.String + ID field.String + CreatedAt field.Time + UpdatedAt field.Time + Name field.String + IsDefault field.Bool + ParentPath field.String + CreatedBy field.String fieldMap map[string]field.Expr } @@ -75,9 +73,8 @@ func (d *docFolder) updateTableName(table string) *docFolder { d.CreatedAt = field.NewTime(table, "created_at") d.UpdatedAt = field.NewTime(table, "updated_at") d.Name = field.NewString(table, "name") - d.IsDeletable = field.NewBool(table, "is_deletable") - d.IsEditable = field.NewBool(table, "is_editable") - d.Path = field.NewString(table, "path") + d.IsDefault = field.NewBool(table, "is_default") + d.ParentPath = field.NewString(table, "parent_path") d.CreatedBy = field.NewString(table, "created_by") d.fillFieldMap() @@ -95,15 +92,14 @@ func (d *docFolder) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (d *docFolder) fillFieldMap() { - d.fieldMap = make(map[string]field.Expr, 9) + d.fieldMap = make(map[string]field.Expr, 8) d.fieldMap["org_id"] = d.OrgID d.fieldMap["id"] = d.ID d.fieldMap["created_at"] = d.CreatedAt d.fieldMap["updated_at"] = d.UpdatedAt d.fieldMap["name"] = d.Name - d.fieldMap["is_deletable"] = d.IsDeletable - d.fieldMap["is_editable"] = d.IsEditable - d.fieldMap["path"] = d.Path + d.fieldMap["is_default"] = d.IsDefault + d.fieldMap["parent_path"] = d.ParentPath d.fieldMap["created_by"] = d.CreatedBy } diff --git a/internal/dal/storage.go b/internal/dal/storage.go index 05711d1..b4f1d0b 100644 --- a/internal/dal/storage.go +++ b/internal/dal/storage.go @@ -22,7 +22,7 @@ func GetStorage() *storage.StorageMinio { func initMinio() { log.Info().Msg("loading minio configs") - s, err := storage.NewObjectStorage(config.Get().Databases.OSS) + s, err := storage.NewObjectStorage(config.Get().Databases.Storage) if err != nil { log.Fatal().Msgf("storage client init failed: %v", err) } diff --git a/internal/router/auth.go b/internal/router/auth.go new file mode 100644 index 0000000..e981088 --- /dev/null +++ b/internal/router/auth.go @@ -0,0 +1,61 @@ +package router + +import ( + "github.com/casdoor/casdoor-go-sdk/casdoorsdk" + "github.com/gofiber/fiber/v2" + "github.com/neo-f/soda" + "github.com/rs/zerolog/log" +) + +func RegisterAuthRouter(app *soda.Soda) { + app.Get("/auth/sign-in", GetSignInURL). + AddTags("Auth"). + SetSummary("登录"). + SetParameters(OauthSchema{}). + OK() + + app.Get("/auth/callback", TokenCallback). + AddTags("Auth"). + SetSummary("Oauth回调地址"). + SetParameters(OauthSchema{}). + OK() +} + +type OauthSchema struct { + Code string `query:"code"` + State string `query:"state"` +} + +func TokenCallback(c *fiber.Ctx) error { + k := c.Locals(soda.KeyParameter).(*OauthSchema) + token, err := casdoorsdk.GetOAuthToken(k.Code, k.State) + if err != nil { + return err + } + return c.JSON(token) +} + +func GetSignInURL(c *fiber.Ctx) error { + url := casdoorsdk.GetSigninUrl("http://localhost:8080/auth/callback") + return c.Redirect(url) +} + +var userKey = struct{}{} + +func JWTRequired(c *fiber.Ctx) error { + jwt := c.Get("Authorization") + if jwt == "" { + return fiber.ErrUnauthorized + } + claims, err := casdoorsdk.ParseJwtToken(jwt[7:]) + if err != nil { + log.Ctx(c.UserContext()).Error().Err(err).Msg("Unauthorized user") + return fiber.ErrUnauthorized + } + c.Locals(userKey, &claims.User) + return nil +} + +func getAuth(c *fiber.Ctx) *casdoorsdk.User { + return c.Locals(userKey).(*casdoorsdk.User) +} diff --git a/internal/router/base.go b/internal/router/base.go deleted file mode 100644 index c2834d1..0000000 --- a/internal/router/base.go +++ /dev/null @@ -1,23 +0,0 @@ -package router - -import ( - "github.com/casdoor/casdoor-go-sdk/casdoorsdk" - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog/log" -) - -var userKey = struct{}{} - -func JWTRequired(c *fiber.Ctx) error { - jwt := c.Get("Authorization") - if jwt == "" { - return fiber.ErrUnauthorized - } - claims, err := casdoorsdk.ParseJwtToken(jwt) - if err != nil { - log.Ctx(c.UserContext()).Error().Err(err).Msg("Unauthorized user") - return fiber.ErrUnauthorized - } - c.Locals(userKey, claims.User) - return nil -} diff --git a/internal/router/doc.go b/internal/router/doc.go index bf3181f..2b3eb62 100644 --- a/internal/router/doc.go +++ b/internal/router/doc.go @@ -4,84 +4,50 @@ import ( "octopus/internal/schema" "octopus/internal/service" - "github.com/casdoor/casdoor-go-sdk/casdoorsdk" "github.com/gofiber/fiber/v2" "github.com/neo-f/soda" ) func RegisterDocRouter(app *soda.Soda) { - registerDocs(app) - registerDocFolders(app) -} - -func registerDocFolders(app *soda.Soda) { - app.Get("/doc-folders", nil). - AddTags("文件夹管理"). - SetSummary("获取文件夹树"). - AddJWTSecurity(JWTRequired). - SetParameters(schema.GetDocFolderTree{}). - AddJSONResponse(200, schema.DocFolderWithChildren{}).OK() - app.Post("/doc-folders", nil). - AddTags("文件夹管理"). - SetSummary("新建文件夹"). - AddJWTSecurity(JWTRequired). - SetJSONRequestBody(schema.CreateDocFolder{}). - AddJSONResponse(200, schema.DocFolder{}).OK() - app.Put("/doc-folders/:id", nil). - AddTags("文件夹管理"). - SetSummary("更新文件夹"). - AddJWTSecurity(JWTRequired). - SetParameters(schema.DocFolderID{}). - SetJSONRequestBody(schema.UpdateDocFolder{}). - AddJSONResponse(200, schema.DocFolder{}).OK() - app.Delete("/doc-folders/:id", nil). - AddTags("文件夹管理"). - SetSummary("删除文件夹"). - AddJWTSecurity(JWTRequired). - SetParameters(schema.DocFolderID{}). - AddJSONResponse(200, nil).OK() -} - -func registerDocs(app *soda.Soda) { - app.Get("/docs", nil). + app.Get("/docs", ListDocs). AddTags("文档管理"). SetSummary("获取文档列表"). AddJWTSecurity(JWTRequired). SetParameters(schema.ListDocQuery{}). AddJSONResponse(200, schema.DocList{}).OK() - app.Post("/docs", nil). + app.Post("/docs", CreateDoc). AddTags("文档管理"). SetSummary("新建文档"). AddJWTSecurity(JWTRequired). SetJSONRequestBody(schema.CreateDoc{}). AddJSONResponse(200, schema.Doc{}).OK() - app.Put("/docs/:id", nil). + app.Put("/docs/:id", UpdateDoc). AddTags("文档管理"). SetSummary("更新文档"). AddJWTSecurity(JWTRequired). SetParameters(schema.DocID{}). SetJSONRequestBody(schema.UpdateDoc{}). AddJSONResponse(200, schema.Doc{}).OK() - app.Delete("/docs/:id", nil). + app.Delete("/docs/:id", DeleteDoc). AddTags("文档管理"). SetSummary("获取文档列表"). AddJWTSecurity(JWTRequired). SetParameters(schema.DocID{}). AddJSONResponse(200, nil).OK() - app.Post("/docs/batch/delete", nil). + app.Post("/docs/batch/delete", DeleteDoc). AddTags("文档管理"). SetSummary("批量-文件删除"). AddJWTSecurity(JWTRequired). SetJSONRequestBody(schema.DocsBatchDelete{}). AddJSONResponse(200, schema.DocBatchResults{}).OK() - app.Post("/docs/batch/update", nil). + app.Post("/docs/batch/update", UpdateDoc). AddTags("文档管理"). SetSummary("批量-文件更新"). AddJWTSecurity(JWTRequired). SetJSONRequestBody(schema.DocsBatchUpdate{}). AddJSONResponse(200, schema.DocBatchResults{}).OK() // get presigned url for tmp file upload - app.Get("/docs/upload-url", nil). + app.Get("/docs/upload-url", CreateUploadURL). AddTags("文档管理"). SetSummary("获取临时上传文件用的预签名URL"). AddJWTSecurity(JWTRequired). @@ -90,7 +56,7 @@ func registerDocs(app *soda.Soda) { } func ListDocs(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) params := c.Locals(soda.KeyParameter).(*schema.ListDocQuery) docs, total, err := service.ListDocs(c.UserContext(), auth, params) if err != nil { @@ -104,7 +70,7 @@ func ListDocs(c *fiber.Ctx) error { } func CreateDoc(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) body := c.Locals(soda.KeyRequestBody).(*schema.CreateDoc) doc, err := service.CreateDoc(c.UserContext(), auth, body) @@ -115,7 +81,7 @@ func CreateDoc(c *fiber.Ctx) error { } func UpdateDoc(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) params := c.Locals(soda.KeyParameter).(*schema.DocID) body := c.Locals(soda.KeyRequestBody).(*schema.UpdateDoc) @@ -127,7 +93,7 @@ func UpdateDoc(c *fiber.Ctx) error { return c.JSON(doc.ToSchema(ctx)) } func DeleteDoc(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) params := c.Locals(soda.KeyParameter).(*schema.DocID) ctx := c.UserContext() @@ -138,7 +104,7 @@ func DeleteDoc(c *fiber.Ctx) error { } func DeleteDocBatch(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) body := c.Locals(soda.KeyRequestBody).(*schema.DocsBatchDelete) ctx := c.UserContext() @@ -147,7 +113,7 @@ func DeleteDocBatch(c *fiber.Ctx) error { } func UpdateDocBatch(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) body := c.Locals(soda.KeyRequestBody).(*schema.DocsBatchUpdate) ctx := c.UserContext() @@ -156,7 +122,7 @@ func UpdateDocBatch(c *fiber.Ctx) error { } func CreateUploadURL(c *fiber.Ctx) error { - auth := c.Locals(userKey).(*casdoorsdk.User) + auth := getAuth(c) params := c.Locals(soda.KeyParameter).(*schema.CreateUploadURL) ctx := c.UserContext() diff --git a/internal/router/doc_folder.go b/internal/router/doc_folder.go new file mode 100644 index 0000000..45abbeb --- /dev/null +++ b/internal/router/doc_folder.go @@ -0,0 +1,96 @@ +package router + +import ( + "octopus/internal/schema" + "octopus/internal/service" + + "github.com/gofiber/fiber/v2" + "github.com/neo-f/soda" +) + +func RegisterDocFolderRouters(app *soda.Soda) { + app.Get("/doc-folders", GetDocFolderTree). + AddTags("文件夹管理"). + SetSummary("获取文件夹树"). + AddJWTSecurity(JWTRequired). + SetParameters(schema.GetDocFolderTree{}). + AddJSONResponse(200, schema.DocFolderWithChildren{}). + OK() + + app.Post("/doc-folders", CreateDocFolder). + AddTags("文件夹管理"). + SetSummary("新建文件夹"). + AddJWTSecurity(JWTRequired). + SetJSONRequestBody(schema.CreateDocFolder{}). + AddJSONResponse(200, schema.DocFolder{}). + OK() + + app.Put("/doc-folders/:id", UpdateDocFolder). + AddTags("文件夹管理"). + SetSummary("更新文件夹"). + AddJWTSecurity(JWTRequired). + SetParameters(schema.DocFolderID{}). + SetJSONRequestBody(schema.UpdateDocFolder{}). + AddJSONResponse(200, schema.DocFolder{}). + OK() + + app.Delete("/doc-folders/:id", DeleteDocFolder). + AddTags("文件夹管理"). + SetSummary("删除文件夹"). + AddJWTSecurity(JWTRequired). + SetParameters(schema.DocFolderID{}). + AddJSONResponse(200, nil). + OK() +} + +func GetDocFolderTree(c *fiber.Ctx) error { + auth := getAuth(c) + param := c.Locals(soda.KeyParameter).(*schema.GetDocFolderTree) + ctx := c.UserContext() + + tree, err := service.GetDocFolderTree(ctx, auth, param) + if err != nil { + return err + } + schemas := make([]*schema.DocFolderWithChildren, len(tree)) + for i, t := range tree { + schemas[i] = t.ToTreeSchema() + } + return c.JSON(schemas) +} + +func CreateDocFolder(c *fiber.Ctx) error { + auth := getAuth(c) + body := c.Locals(soda.KeyRequestBody).(*schema.CreateDocFolder) + ctx := c.UserContext() + + doc, err := service.CreateDocFolder(ctx, auth, body) + if err != nil { + return err + } + return c.JSON(doc.ToSchema()) +} + +func UpdateDocFolder(c *fiber.Ctx) error { + auth := getAuth(c) + param := c.Locals(soda.KeyParameter).(*schema.DocFolderID) + body := c.Locals(soda.KeyRequestBody).(*schema.UpdateDocFolder) + ctx := c.UserContext() + + doc, err := service.UpdateDocFolder(ctx, auth, param.ID, body) + if err != nil { + return err + } + return c.JSON(doc.ToSchema()) +} + +func DeleteDocFolder(c *fiber.Ctx) error { + auth := getAuth(c) + param := c.Locals(soda.KeyParameter).(*schema.DocFolderID) + ctx := c.UserContext() + + if err := service.DeleteDocFolder(ctx, auth, param.ID); err != nil { + return err + } + return c.JSON(nil) +} diff --git a/internal/schema/docs.go b/internal/schema/docs.go index 3dbc7f9..ba9a359 100644 --- a/internal/schema/docs.go +++ b/internal/schema/docs.go @@ -6,11 +6,10 @@ import ( ) type DocFolder struct { - ID string `json:"id" oai:"description=文件夹ID"` - IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"` - IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"` - CreatedAt time.Time `json:"created_at" oai:"description=创建时间"` - UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"` + ID string `json:"id" oai:"description=文件夹ID"` + Name string `json:"name" oai:"description=文件夹名称"` + CreatedAt time.Time `json:"created_at" oai:"description=创建时间"` + UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"` } type DocFolderID struct { @@ -32,20 +31,14 @@ type GetDocFolderTree struct { } type DocFolderWithChildren struct { - ID string `json:"id" oai:"description=文件夹ID,**根文件夹无ID**"` - IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"` - IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"` - CreatedAt time.Time `json:"created_at" oai:"description=创建时间"` - UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"` - Children []*DocFolderWithChildren `json:"children" oai:"description=子文件夹"` + *DocFolder + Children []*DocFolderWithChildren `json:"children,omitempty" oai:"description=子文件夹;required=false"` } type Doc struct { ID string `json:"id" oai:"description=文档ID"` Folder *DocFolder `json:"folder" oai:"description=归属文件夹信息"` PresignedURL *url.URL `json:"presigned_url" oai:"description=文档预签名下载URL(临时下载URL)"` - IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"` - IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"` UploadedAt time.Time `json:"uploaded_at" oai:"description=上传时间"` CreatedAt time.Time `json:"created_at" oai:"description=创建时间"` UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"` diff --git a/internal/service/doc.go b/internal/service/doc.go index 3ca680d..b858d05 100644 --- a/internal/service/doc.go +++ b/internal/service/doc.go @@ -9,6 +9,7 @@ import ( "octopus/internal/dal/model" "octopus/internal/dal/query" "octopus/internal/schema" + "strings" "time" "github.com/casdoor/casdoor-go-sdk/casdoorsdk" @@ -46,7 +47,7 @@ func CreateDoc(ctx context.Context, auth *casdoorsdk.User, param *schema.CreateD } // 将对象从临时地址移动到新地址 - newPath := fmt.Sprintf("%s/%s/%s", auth.Owner, folder.Path, param.Name) + newPath := fmt.Sprintf("%s/%s/%s", auth.Owner, folder.ParentPath, param.Name) if err := storage.MoveObject(ctx, param.ObjectName, newPath); err != nil { return nil, fmt.Errorf("failed to move object: %w", err) } @@ -90,17 +91,17 @@ func UpdateDoc(ctx context.Context, auth *casdoorsdk.User, id string, body *sche } if body.FolderID != nil { - folder, err := GetDocFolder(ctx, auth, *body.FolderID) - if err != nil { + if _, err := GetDocFolder(ctx, auth, *body.FolderID); err != nil { return nil, fmt.Errorf("failed to get doc folder: %w", err) } // 将对象从临时地址移动到新地址 - newObjectName := fmt.Sprintf("%s/%s/%s", auth.Owner, folder.Path, doc.Name) + parts := strings.Split(doc.ObjectName, "/") + objname := parts[len(parts)-1] + newObjectName := fmt.Sprintf("%s/%s/%s", auth.Owner, time.Now().UTC().Format(time.DateOnly), objname) if err := dal.GetStorage().MoveObject(ctx, doc.ObjectName, newObjectName); err != nil { return nil, fmt.Errorf("failed to move object: %w", err) } - updates = append(updates, query.Doc.ObjectName.Value(newObjectName)) updates = append(updates, query.Doc.FolderID.Value(*body.FolderID)) } @@ -162,14 +163,3 @@ func CreateUploadURL(ctx context.Context, auth *casdoorsdk.User, param *schema.C } return u, tmpObjectName, nil } - -func GetDocFolder(ctx context.Context, auth *casdoorsdk.User, id string) (*model.DocFolder, error) { - folder, err := query.DocFolder.Where(query.DocFolder.OrgID.Eq(auth.Owner), query.DocFolder.ID.Eq(id)).Take() - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("specified doc folder not exists") - } - if err != nil { - return nil, err - } - return folder, nil -} diff --git a/internal/service/doc_folder.go b/internal/service/doc_folder.go new file mode 100644 index 0000000..838be54 --- /dev/null +++ b/internal/service/doc_folder.go @@ -0,0 +1,203 @@ +package service + +import ( + "context" + "errors" + "fmt" + "octopus/internal/dal" + "octopus/internal/dal/model" + "octopus/internal/dal/query" + "octopus/internal/schema" + + "github.com/casdoor/casdoor-go-sdk/casdoorsdk" + "gorm.io/gen/field" + "gorm.io/gorm" +) + +func GetDocFolderTree(ctx context.Context, auth *casdoorsdk.User, param *schema.GetDocFolderTree) ([]*model.DocFolderWithChildren, error) { + parentPath := "" + parentID := "" + if param.ParentID != nil { + parent, err := GetDocFolder(ctx, auth, *param.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent folder: %w", err) + } + parentPath = parent.ParentPath + parentID = parent.ID + } + + folders, err := query.DocFolder. + Where( + query.DocFolder.OrgID.Eq(auth.Owner), + query.DocFolder.ParentPath.Like(parentPath+"%"), + ). + Order(query.DocFolder.ParentPath). + Find() + if err != nil { + return nil, fmt.Errorf("failed to get doc folders: %w", err) + } + + // 如果没有的话,就创建一个默认的分组 + if len(folders) == 0 { + folder := model.DocFolder{ + Base: model.Base{OrgID: auth.Owner}, + Name: "默认分组", + IsDefault: true, + ParentPath: parentPath, + CreatedBy: auth.Id, + } + if err := query.DocFolder.Create(&folder); err != nil { + return nil, fmt.Errorf("failed to create doc folder: %w", err) + } + + folders = append(folders, &folder) + } + + return composeFolders(parentID, folders), nil +} + +func GetDocFolder(ctx context.Context, auth *casdoorsdk.User, id string) (*model.DocFolder, error) { + folder, err := query.DocFolder.Where(query.DocFolder.OrgID.Eq(auth.Owner), query.DocFolder.ID.Eq(id)).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("specified doc folder not exists") + } + if err != nil { + return nil, err + } + return folder, nil +} + +func GetDocFolderChildren(ctx context.Context, auth *casdoorsdk.User, id string) ([]*model.DocFolder, error) { + folder, err := GetDocFolder(ctx, auth, id) + if err != nil { + return nil, err + } + return query.DocFolder.Where(query.DocFolder.OrgID.Eq(auth.Owner), query.DocFolder.ParentPath.Like(folder.ParentPath+"%")).Find() +} + +func CreateDocFolder(ctx context.Context, auth *casdoorsdk.User, param *schema.CreateDocFolder) (*model.DocFolder, error) { + path := "" + if param.ParentID != "" { + parent, err := GetDocFolder(ctx, auth, param.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent folder: %w", err) + } + path = fmt.Sprintf("%s/%s", parent.ParentPath, parent.ID) + } + + folder := model.DocFolder{ + Base: model.Base{OrgID: auth.Owner}, + Name: param.Name, + ParentPath: path, + CreatedBy: auth.Id, + } + if err := query.DocFolder.Create(&folder); err != nil { + return nil, fmt.Errorf("failed to create doc folder: %w", err) + } + return &folder, nil +} + +func UpdateDocFolder(ctx context.Context, auth *casdoorsdk.User, id string, param *schema.UpdateDocFolder) (*model.DocFolder, error) { + folder, err := GetDocFolder(ctx, auth, id) + if err != nil { + return nil, fmt.Errorf("failed to get doc folder: %w", err) + } + + var updates []field.AssignExpr + + if param.ParentID != nil { + if folder.IsDefault { + return nil, fmt.Errorf("cannot move the default folder") + } + + parent, err := GetDocFolder(ctx, auth, *param.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent folder: %w", err) + } + newParentPath := fmt.Sprintf("%s/%s", parent.ParentPath, parent.ID) + updates = append(updates, query.DocFolder.ParentPath.Value(newParentPath)) + } + + if param.Name != nil { + updates = append(updates, query.DocFolder.Name.Value(*param.Name)) + } + if _, err := query.DocFolder.Where(query.DocFolder.ID.Eq(id)).UpdateSimple(updates...); err != nil { + return nil, fmt.Errorf("failed to update doc folder: %w", err) + } + return GetDocFolder(ctx, auth, id) +} + +func DeleteDocFolder(ctx context.Context, auth *casdoorsdk.User, id string) error { + folder, err := GetDocFolder(ctx, auth, id) + if err != nil { + return fmt.Errorf("failed to get doc folder: %w", err) + } + if folder.IsDefault { + return fmt.Errorf("cannot delete the default folder") + } + subFolders, err := GetDocFolderChildren(ctx, auth, id) + if err != nil { + return fmt.Errorf("failed to get doc folder children: %w", err) + } + + allFolderIDs := make([]string, 0, len(subFolders)+1) + allFolderIDs = append(allFolderIDs, folder.ID) + for _, subFolder := range subFolders { + allFolderIDs = append(allFolderIDs, subFolder.ID) + } + + // 需要把文件和对应的对象存储中的文件一并删除 + docs, err := query.Doc.Where(query.Doc.OrgID.Eq(auth.Owner), query.Doc.FolderID.In(allFolderIDs...)).Find() + if err != nil { + return fmt.Errorf("failed to get docs: %w", err) + } + + objectNames := make([]string, 0, len(docs)) + docIDs := make([]string, 0, len(docs)) + for _, doc := range docs { + objectNames = append(objectNames, doc.ObjectName) + docIDs = append(docIDs, doc.ID) + } + + if len(objectNames) > 0 { + if errs := dal.GetStorage().DeleteObjects(ctx, objectNames); len(errs) != 0 { + err := errors.New("failed to delete objects") + for _, e := range errs { + err = fmt.Errorf("failed to delete object %s: %w", e.ObjectName, e.Err) + } + return err + } + } + if len(docIDs) > 0 { + if _, err := query.Doc.Where(query.Doc.ID.In(docIDs...)).Delete(); err != nil { + return fmt.Errorf("failed to delete docs: %w", err) + } + } + + if _, err := query.DocFolder.Where(query.DocFolder.ID.In(allFolderIDs...)).Delete(); err != nil { + return fmt.Errorf("failed to delete doc folder: %w", err) + } + + return nil +} + +// composeFolders() 用于把文件夹列表转换成树形结构 +func composeFolders(topID string, folders []*model.DocFolder) []*model.DocFolderWithChildren { + m := make(map[string]*model.DocFolderWithChildren) + for _, folder := range folders { + m[folder.ID] = &model.DocFolderWithChildren{DocFolder: folder} + } + + results := make([]*model.DocFolderWithChildren, 0) + for _, folder := range folders { + parentID := folder.ParentID() + if parentID == topID { + results = append(results, m[folder.ID]) + continue + } + if parent, ok := m[parentID]; ok { + parent.Children = append(parent.Children, m[folder.ID]) + } + } + return results +} diff --git a/pkg/logger/setup.go b/pkg/logger/setup.go index 83f579a..6c4cd11 100644 --- a/pkg/logger/setup.go +++ b/pkg/logger/setup.go @@ -2,7 +2,6 @@ package logger import ( "io" - "os" "octopus" "octopus/internal/config" @@ -17,11 +16,11 @@ func Setup() { zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack writers := []io.Writer{} - if config.Get().IsLocal { - writers = append(writers, zerolog.NewConsoleWriter()) - } else { - writers = append(writers, os.Stderr) - } + // if config.Get().IsLocal { + writers = append(writers, zerolog.NewConsoleWriter()) + // } else { + // writers = append(writers, os.Stderr) + // } if dsn := cfg.Sentry.DSN; dsn != "" { sentryWriter, err := NewSentryWriter( cfg.Sentry.DSN, diff --git a/pkg/storage/storage_minio.go b/pkg/storage/storage_minio.go index 346ac6a..96dafcd 100644 --- a/pkg/storage/storage_minio.go +++ b/pkg/storage/storage_minio.go @@ -83,3 +83,20 @@ func (m *StorageMinio) DeleteObject(ctx context.Context, objectName string) (err } return nil } + +// MoveObject implements ObjectStorage +func (m *StorageMinio) DeleteObjects(ctx context.Context, objectNames []string) []minio.RemoveObjectError { + objs := make(chan minio.ObjectInfo, len(objectNames)) + for _, objectName := range objectNames { + objs <- minio.ObjectInfo{Key: objectName} + } + errCh := m.client.RemoveObjects(ctx, m.bucket, objs, minio.RemoveObjectsOptions{}) + + var errors []minio.RemoveObjectError + for err := range errCh { + if err.Err != nil { + errors = append(errors, err) + } + } + return errors +}