feat(api): doc folders curd implemention

This commit is contained in:
neo-f 2023-03-23 21:27:28 +08:00
parent 28edda5c7a
commit 6851fe95a0
17 changed files with 539 additions and 155 deletions

View File

@ -3,9 +3,11 @@ package main
import ( import (
"octopus/cmd/octopus/scripts" "octopus/cmd/octopus/scripts"
"octopus/cmd/octopus/server" "octopus/cmd/octopus/server"
"octopus/internal/config"
"octopus/internal/dal" "octopus/internal/dal"
"octopus/internal/dal/query" "octopus/internal/dal/query"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -19,8 +21,17 @@ var rootCmd = &cobra.Command{
func main() { func main() {
rootCmd.AddCommand(server.CmdRun) rootCmd.AddCommand(server.CmdRun)
rootCmd.AddCommand(scripts.CmdScripts) rootCmd.AddCommand(scripts.CmdScripts)
query.SetDefault(dal.GetPostgres()) db := dal.GetPostgres()
// migrations.Migrate(db)
query.SetDefault(db)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
log.Fatal().Err(err).Msg("Failed to execute root command") 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)
}

View File

@ -1,7 +1,6 @@
package server package server
import ( import (
"net/http"
"time" "time"
"octopus" "octopus"
@ -30,8 +29,10 @@ var innerURLs = tools.NewSet(
var ROUTES = []func(app *soda.Soda){ var ROUTES = []func(app *soda.Soda){
RegisterBase, RegisterBase,
router.RegisterAuthRouter,
router.RegisterDebuggerRouter, router.RegisterDebuggerRouter,
router.RegisterDocRouter, router.RegisterDocRouter,
router.RegisterDocFolderRouters,
} }
// startHttpServer starts configures and starts an HTTP server on the given URL. // startHttpServer starts configures and starts an HTTP server on the given URL.
@ -52,8 +53,8 @@ func InitApp() *soda.Soda {
if err == nil { if err == nil {
return c.Next() return c.Next()
} }
status := http.StatusInternalServerError //default error status status := 400 //default error status
if e, ok := err.(*fiber.Error); ok { // it's a custom error, so use the status in the error if e, ok := err.(*fiber.Error); ok { // it's a custom error, so use the status in the error
status = e.Code status = e.Code
} }
msg := map[string]interface{}{"code": status, "message": err.Error()} msg := map[string]interface{}{"code": status, "message": err.Error()}

View File

@ -1,7 +1,44 @@
debug: true debug: true
http_port: 8080 http_port: 8080
prometheus_port: 18090 prometheus_port: 18090
host: http://localhost:8080
databases: 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 postgresql: postgres://postgres:TvpHKxDhXzMsVkXWdzwiwUw9KmaLuGdRJo@10.16.129.140:31258/octopus
qdrant: 10.16.129.104:32352 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

View File

@ -13,20 +13,30 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type Config struct { type casdoorConfig struct {
IsLocal bool Endpoint string `validate:"required" mapstructure:"endpoint"`
Debug bool `mapstructure:"debug"` ClientID string `validate:"required" mapstructure:"client_id"`
HTTPPort int `mapstructure:"http_port" validate:"required"` 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 { Databases struct {
OSS string `mapstructure:"oss" validate:"required"` Storage string `mapstructure:"storage" validate:"required"`
PostgreSQL string `mapstructure:"postgresql" validate:"required"` PostgreSQL string `mapstructure:"postgresql" validate:"required"`
Qdrant string `mapstructure:"qdrant" validate:"required"` Qdrant string `mapstructure:"qdrant" validate:"required"`
} `mapstructure:"databases"` } `mapstructure:"databases"`
Sentry struct { Sentry struct {
Environment string Environment string `mapstructure:"environment"`
DSN string DSN string `mapstructure:"dsn"`
} } `mapstructure:"sentry"`
} }
var ( var (

View File

@ -3,8 +3,8 @@ package model
import "time" import "time"
type Base struct { type Base struct {
OrgID string `gorm:"column:org_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"` // ID ID string `gorm:"column:id;type:varchar;primaryKey;comment:ID"`
CreatedAt time.Time `gorm:"column:created_at;comment:创建时间"` CreatedAt time.Time `gorm:"column:created_at;comment:创建时间"`
UpdatedAt time.Time `gorm:"column:updated_at;comment:更新时间"` UpdatedAt time.Time `gorm:"column:updated_at;comment:更新时间"`
} }

View File

@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"octopus/internal/dal" "octopus/internal/dal"
"octopus/internal/schema" "octopus/internal/schema"
"strings"
"time" "time"
"github.com/rs/xid" "github.com/rs/xid"
@ -16,24 +17,29 @@ const TableNameDoc = "docs"
type DocFolder struct { type DocFolder struct {
Base Base
Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称 Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称
IsDeletable bool `gorm:"column:is_deletable;type:boolean;not null;default:true"` // 是否允许被删除 IsDefault bool `gorm:"column:is_default;type:boolean;not null;default:false"` // 是否默认分组
IsEditable bool `gorm:"column:is_editable;type:boolean;not null;default:true"` // 是否允许被修改 ParentPath string `gorm:"column:parent_path;type:varchar;index:idx_path"` // 路径索引
Path string `gorm:"column:path;type:varchar;index:idx_path"` // 路径索引 CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人
CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人
} }
func (*DocFolder) TableName() string { func (*DocFolder) TableName() string {
return TableNameDocFolder 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 { func (df *DocFolder) ToSchema() *schema.DocFolder {
return &schema.DocFolder{ return &schema.DocFolder{
ID: df.ID, ID: df.ID,
IsDeletable: df.IsDeletable, Name: df.Name,
IsEditable: df.IsEditable, CreatedAt: df.CreatedAt,
CreatedAt: df.CreatedAt, UpdatedAt: df.UpdatedAt,
UpdatedAt: df.UpdatedAt,
} }
} }
@ -42,6 +48,28 @@ func (d *DocFolder) BeforeCreate(*gorm.DB) error {
return nil 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 { type Doc struct {
Base Base
Name string `gorm:"column:name;type:varchar;not null"` // 文件夹名称 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"` // 是否允许编辑名称 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 FolderID string `gorm:"column:folder_id;type:varchar;index:idx_folder_id"` // 文件夹ID
ObjectName string `gorm:"column:object_name;type:varchar"` // 对象存储中对应的object_name 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"` // 创建人 CreatedBy string `gorm:"column:created_by;type:varchar;not null"` // 创建人
Folder *DocFolder `gorm:"foreignKey:FolderID;references:ID"` Folder *DocFolder `gorm:"foreignKey:FolderID;references:ID"`
@ -62,8 +90,6 @@ func (d *Doc) ToSchema(ctx context.Context) *schema.Doc {
ID: d.ID, ID: d.ID,
Folder: d.Folder.ToSchema(), Folder: d.Folder.ToSchema(),
PresignedURL: url, PresignedURL: url,
IsDeletable: d.IsDeletable,
IsEditable: d.IsEditable,
UploadedAt: d.UploadedAt, UploadedAt: d.UploadedAt,
CreatedAt: d.CreatedAt, CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt, UpdatedAt: d.UpdatedAt,
@ -77,6 +103,7 @@ func (d *Doc) PresignedURL(ctx context.Context) (*url.URL, error) {
func (*Doc) TableName() string { func (*Doc) TableName() string {
return TableNameDoc return TableNameDoc
} }
func (d *Doc) BeforeCreate(*gorm.DB) error { func (d *Doc) BeforeCreate(*gorm.DB) error {
d.ID = xid.New().String() d.ID = xid.New().String()
return nil return nil

View File

@ -31,9 +31,8 @@ func newDocFolder(db *gorm.DB, opts ...gen.DOOption) docFolder {
_docFolder.CreatedAt = field.NewTime(tableName, "created_at") _docFolder.CreatedAt = field.NewTime(tableName, "created_at")
_docFolder.UpdatedAt = field.NewTime(tableName, "updated_at") _docFolder.UpdatedAt = field.NewTime(tableName, "updated_at")
_docFolder.Name = field.NewString(tableName, "name") _docFolder.Name = field.NewString(tableName, "name")
_docFolder.IsDeletable = field.NewBool(tableName, "is_deletable") _docFolder.IsDefault = field.NewBool(tableName, "is_default")
_docFolder.IsEditable = field.NewBool(tableName, "is_editable") _docFolder.ParentPath = field.NewString(tableName, "parent_path")
_docFolder.Path = field.NewString(tableName, "path")
_docFolder.CreatedBy = field.NewString(tableName, "created_by") _docFolder.CreatedBy = field.NewString(tableName, "created_by")
_docFolder.fillFieldMap() _docFolder.fillFieldMap()
@ -44,16 +43,15 @@ func newDocFolder(db *gorm.DB, opts ...gen.DOOption) docFolder {
type docFolder struct { type docFolder struct {
docFolderDo docFolderDo
ALL field.Asterisk ALL field.Asterisk
OrgID field.String OrgID field.String
ID field.String ID field.String
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
Name field.String Name field.String
IsDeletable field.Bool IsDefault field.Bool
IsEditable field.Bool ParentPath field.String
Path field.String CreatedBy field.String
CreatedBy field.String
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@ -75,9 +73,8 @@ func (d *docFolder) updateTableName(table string) *docFolder {
d.CreatedAt = field.NewTime(table, "created_at") d.CreatedAt = field.NewTime(table, "created_at")
d.UpdatedAt = field.NewTime(table, "updated_at") d.UpdatedAt = field.NewTime(table, "updated_at")
d.Name = field.NewString(table, "name") d.Name = field.NewString(table, "name")
d.IsDeletable = field.NewBool(table, "is_deletable") d.IsDefault = field.NewBool(table, "is_default")
d.IsEditable = field.NewBool(table, "is_editable") d.ParentPath = field.NewString(table, "parent_path")
d.Path = field.NewString(table, "path")
d.CreatedBy = field.NewString(table, "created_by") d.CreatedBy = field.NewString(table, "created_by")
d.fillFieldMap() d.fillFieldMap()
@ -95,15 +92,14 @@ func (d *docFolder) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (d *docFolder) fillFieldMap() { 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["org_id"] = d.OrgID
d.fieldMap["id"] = d.ID d.fieldMap["id"] = d.ID
d.fieldMap["created_at"] = d.CreatedAt d.fieldMap["created_at"] = d.CreatedAt
d.fieldMap["updated_at"] = d.UpdatedAt d.fieldMap["updated_at"] = d.UpdatedAt
d.fieldMap["name"] = d.Name d.fieldMap["name"] = d.Name
d.fieldMap["is_deletable"] = d.IsDeletable d.fieldMap["is_default"] = d.IsDefault
d.fieldMap["is_editable"] = d.IsEditable d.fieldMap["parent_path"] = d.ParentPath
d.fieldMap["path"] = d.Path
d.fieldMap["created_by"] = d.CreatedBy d.fieldMap["created_by"] = d.CreatedBy
} }

View File

@ -22,7 +22,7 @@ func GetStorage() *storage.StorageMinio {
func initMinio() { func initMinio() {
log.Info().Msg("loading minio configs") 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 { if err != nil {
log.Fatal().Msgf("storage client init failed: %v", err) log.Fatal().Msgf("storage client init failed: %v", err)
} }

61
internal/router/auth.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -4,84 +4,50 @@ import (
"octopus/internal/schema" "octopus/internal/schema"
"octopus/internal/service" "octopus/internal/service"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/neo-f/soda" "github.com/neo-f/soda"
) )
func RegisterDocRouter(app *soda.Soda) { func RegisterDocRouter(app *soda.Soda) {
registerDocs(app) app.Get("/docs", ListDocs).
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).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("获取文档列表"). SetSummary("获取文档列表").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
SetParameters(schema.ListDocQuery{}). SetParameters(schema.ListDocQuery{}).
AddJSONResponse(200, schema.DocList{}).OK() AddJSONResponse(200, schema.DocList{}).OK()
app.Post("/docs", nil). app.Post("/docs", CreateDoc).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("新建文档"). SetSummary("新建文档").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.CreateDoc{}). SetJSONRequestBody(schema.CreateDoc{}).
AddJSONResponse(200, schema.Doc{}).OK() AddJSONResponse(200, schema.Doc{}).OK()
app.Put("/docs/:id", nil). app.Put("/docs/:id", UpdateDoc).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("更新文档"). SetSummary("更新文档").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
SetParameters(schema.DocID{}). SetParameters(schema.DocID{}).
SetJSONRequestBody(schema.UpdateDoc{}). SetJSONRequestBody(schema.UpdateDoc{}).
AddJSONResponse(200, schema.Doc{}).OK() AddJSONResponse(200, schema.Doc{}).OK()
app.Delete("/docs/:id", nil). app.Delete("/docs/:id", DeleteDoc).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("获取文档列表"). SetSummary("获取文档列表").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
SetParameters(schema.DocID{}). SetParameters(schema.DocID{}).
AddJSONResponse(200, nil).OK() AddJSONResponse(200, nil).OK()
app.Post("/docs/batch/delete", nil). app.Post("/docs/batch/delete", DeleteDoc).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("批量-文件删除"). SetSummary("批量-文件删除").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.DocsBatchDelete{}). SetJSONRequestBody(schema.DocsBatchDelete{}).
AddJSONResponse(200, schema.DocBatchResults{}).OK() AddJSONResponse(200, schema.DocBatchResults{}).OK()
app.Post("/docs/batch/update", nil). app.Post("/docs/batch/update", UpdateDoc).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("批量-文件更新"). SetSummary("批量-文件更新").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
SetJSONRequestBody(schema.DocsBatchUpdate{}). SetJSONRequestBody(schema.DocsBatchUpdate{}).
AddJSONResponse(200, schema.DocBatchResults{}).OK() AddJSONResponse(200, schema.DocBatchResults{}).OK()
// get presigned url for tmp file upload // get presigned url for tmp file upload
app.Get("/docs/upload-url", nil). app.Get("/docs/upload-url", CreateUploadURL).
AddTags("文档管理"). AddTags("文档管理").
SetSummary("获取临时上传文件用的预签名URL"). SetSummary("获取临时上传文件用的预签名URL").
AddJWTSecurity(JWTRequired). AddJWTSecurity(JWTRequired).
@ -90,7 +56,7 @@ func registerDocs(app *soda.Soda) {
} }
func ListDocs(c *fiber.Ctx) error { func ListDocs(c *fiber.Ctx) error {
auth := c.Locals(userKey).(*casdoorsdk.User) auth := getAuth(c)
params := c.Locals(soda.KeyParameter).(*schema.ListDocQuery) params := c.Locals(soda.KeyParameter).(*schema.ListDocQuery)
docs, total, err := service.ListDocs(c.UserContext(), auth, params) docs, total, err := service.ListDocs(c.UserContext(), auth, params)
if err != nil { if err != nil {
@ -104,7 +70,7 @@ func ListDocs(c *fiber.Ctx) error {
} }
func CreateDoc(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) body := c.Locals(soda.KeyRequestBody).(*schema.CreateDoc)
doc, err := service.CreateDoc(c.UserContext(), auth, body) doc, err := service.CreateDoc(c.UserContext(), auth, body)
@ -115,7 +81,7 @@ func CreateDoc(c *fiber.Ctx) error {
} }
func UpdateDoc(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) params := c.Locals(soda.KeyParameter).(*schema.DocID)
body := c.Locals(soda.KeyRequestBody).(*schema.UpdateDoc) body := c.Locals(soda.KeyRequestBody).(*schema.UpdateDoc)
@ -127,7 +93,7 @@ func UpdateDoc(c *fiber.Ctx) error {
return c.JSON(doc.ToSchema(ctx)) return c.JSON(doc.ToSchema(ctx))
} }
func DeleteDoc(c *fiber.Ctx) error { func DeleteDoc(c *fiber.Ctx) error {
auth := c.Locals(userKey).(*casdoorsdk.User) auth := getAuth(c)
params := c.Locals(soda.KeyParameter).(*schema.DocID) params := c.Locals(soda.KeyParameter).(*schema.DocID)
ctx := c.UserContext() ctx := c.UserContext()
@ -138,7 +104,7 @@ func DeleteDoc(c *fiber.Ctx) error {
} }
func DeleteDocBatch(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) body := c.Locals(soda.KeyRequestBody).(*schema.DocsBatchDelete)
ctx := c.UserContext() ctx := c.UserContext()
@ -147,7 +113,7 @@ func DeleteDocBatch(c *fiber.Ctx) error {
} }
func UpdateDocBatch(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) body := c.Locals(soda.KeyRequestBody).(*schema.DocsBatchUpdate)
ctx := c.UserContext() ctx := c.UserContext()
@ -156,7 +122,7 @@ func UpdateDocBatch(c *fiber.Ctx) error {
} }
func CreateUploadURL(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) params := c.Locals(soda.KeyParameter).(*schema.CreateUploadURL)
ctx := c.UserContext() ctx := c.UserContext()

View File

@ -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)
}

View File

@ -6,11 +6,10 @@ import (
) )
type DocFolder struct { type DocFolder struct {
ID string `json:"id" oai:"description=文件夹ID"` ID string `json:"id" oai:"description=文件夹ID"`
IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"` Name string `json:"name" oai:"description=文件夹名称"`
IsEditable bool `json:"is_editable" oai:"description=文件夹是否允许被修改"` CreatedAt time.Time `json:"created_at" oai:"description=创建时间"`
CreatedAt time.Time `json:"created_at" oai:"description=创建时间"` UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`
UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`
} }
type DocFolderID struct { type DocFolderID struct {
@ -32,20 +31,14 @@ type GetDocFolderTree struct {
} }
type DocFolderWithChildren struct { type DocFolderWithChildren struct {
ID string `json:"id" oai:"description=文件夹ID**根文件夹无ID**"` *DocFolder
IsDeletable bool `json:"is_deletable" oai:"description=文件夹是否允许被删除"` Children []*DocFolderWithChildren `json:"children,omitempty" oai:"description=子文件夹;required=false"`
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=子文件夹"`
} }
type Doc struct { type Doc struct {
ID string `json:"id" oai:"description=文档ID"` ID string `json:"id" oai:"description=文档ID"`
Folder *DocFolder `json:"folder" oai:"description=归属文件夹信息"` Folder *DocFolder `json:"folder" oai:"description=归属文件夹信息"`
PresignedURL *url.URL `json:"presigned_url" oai:"description=文档预签名下载URL(临时下载URL)"` 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=上传时间"` UploadedAt time.Time `json:"uploaded_at" oai:"description=上传时间"`
CreatedAt time.Time `json:"created_at" oai:"description=创建时间"` CreatedAt time.Time `json:"created_at" oai:"description=创建时间"`
UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"` UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`

View File

@ -9,6 +9,7 @@ import (
"octopus/internal/dal/model" "octopus/internal/dal/model"
"octopus/internal/dal/query" "octopus/internal/dal/query"
"octopus/internal/schema" "octopus/internal/schema"
"strings"
"time" "time"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk" "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 { if err := storage.MoveObject(ctx, param.ObjectName, newPath); err != nil {
return nil, fmt.Errorf("failed to move object: %w", err) 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 { if body.FolderID != nil {
folder, err := GetDocFolder(ctx, auth, *body.FolderID) if _, err := GetDocFolder(ctx, auth, *body.FolderID); err != nil {
if err != nil {
return nil, fmt.Errorf("failed to get doc folder: %w", err) 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 { if err := dal.GetStorage().MoveObject(ctx, doc.ObjectName, newObjectName); err != nil {
return nil, fmt.Errorf("failed to move object: %w", err) return nil, fmt.Errorf("failed to move object: %w", err)
} }
updates = append(updates, query.Doc.ObjectName.Value(newObjectName)) updates = append(updates, query.Doc.ObjectName.Value(newObjectName))
updates = append(updates, query.Doc.FolderID.Value(*body.FolderID)) 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 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
}

View File

@ -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
}

View File

@ -2,7 +2,6 @@ package logger
import ( import (
"io" "io"
"os"
"octopus" "octopus"
"octopus/internal/config" "octopus/internal/config"
@ -17,11 +16,11 @@ func Setup() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
writers := []io.Writer{} writers := []io.Writer{}
if config.Get().IsLocal { // if config.Get().IsLocal {
writers = append(writers, zerolog.NewConsoleWriter()) writers = append(writers, zerolog.NewConsoleWriter())
} else { // } else {
writers = append(writers, os.Stderr) // writers = append(writers, os.Stderr)
} // }
if dsn := cfg.Sentry.DSN; dsn != "" { if dsn := cfg.Sentry.DSN; dsn != "" {
sentryWriter, err := NewSentryWriter( sentryWriter, err := NewSentryWriter(
cfg.Sentry.DSN, cfg.Sentry.DSN,

View File

@ -83,3 +83,20 @@ func (m *StorageMinio) DeleteObject(ctx context.Context, objectName string) (err
} }
return nil 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
}