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 (
"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)
}

View File

@ -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,7 +53,7 @@ func InitApp() *soda.Soda {
if err == nil {
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
status = e.Code
}

View File

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

View File

@ -13,20 +13,30 @@ import (
"github.com/spf13/viper"
)
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 (

View File

@ -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:更新时间"`
}

View File

@ -5,6 +5,7 @@ import (
"net/url"
"octopus/internal/dal"
"octopus/internal/schema"
"strings"
"time"
"github.com/rs/xid"
@ -17,21 +18,26 @@ 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"` // 路径索引
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,
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

View File

@ -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()
@ -50,9 +49,8 @@ type docFolder struct {
CreatedAt field.Time
UpdatedAt field.Time
Name field.String
IsDeletable field.Bool
IsEditable field.Bool
Path 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
}

View File

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

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/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()

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

@ -7,8 +7,7 @@ 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=文件夹是否允许被修改"`
Name string `json:"name" oai:"description=文件夹名称"`
CreatedAt time.Time `json:"created_at" oai:"description=创建时间"`
UpdatedAt time.Time `json:"updated_at" oai:"description=更新时间"`
}
@ -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=更新时间"`

View File

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

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

View File

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