Feat: support mutle alist client

pull/52/head
zijiren233 2 years ago
parent 7d9f4417ae
commit d3431d9004

@ -16,33 +16,32 @@ import (
"github.com/zijiren233/gencontainer/refreshcache"
)
type AlistUserCache = refreshcache.RefreshCache[*AlistUserCacheData, struct{}]
type AlistUserCache = MapCache[*AlistUserCacheData, struct{}]
type AlistUserCacheData struct {
Host string
Token string
Backend string
Host string
ServerID string
Token string
Backend string
}
func NewAlistUserCache(userID string) *AlistUserCache {
f := AlistAuthorizationCacheWithUserIDInitFunc(userID)
return refreshcache.NewRefreshCache(func(ctx context.Context, args ...struct{}) (*AlistUserCacheData, error) {
return f(ctx)
return newMapCache[*AlistUserCacheData, struct{}](func(ctx context.Context, key string, args ...struct{}) (*AlistUserCacheData, error) {
return AlistAuthorizationCacheWithUserIDInitFunc(ctx, userID, key)
}, 0)
}
func AlistAuthorizationCacheWithUserIDInitFunc(userID string) func(ctx context.Context, args ...struct{}) (*AlistUserCacheData, error) {
return func(ctx context.Context, args ...struct{}) (*AlistUserCacheData, error) {
v, err := db.GetAlistVendor(userID)
if err != nil {
return nil, err
}
return AlistAuthorizationCacheWithConfigInitFunc(ctx, v)
func AlistAuthorizationCacheWithUserIDInitFunc(ctx context.Context, userID, serverID string) (*AlistUserCacheData, error) {
v, err := db.GetAlistVendor(userID, serverID)
if err != nil {
return nil, err
}
return AlistAuthorizationCacheWithConfigInitFunc(ctx, v)
}
func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.AlistVendor) (*AlistUserCacheData, error) {
cli := vendor.LoadAlistClient(v.Backend)
model.GenAlistServerID(v)
if v.Username == "" {
_, err := cli.Me(ctx, &alist.MeReq{
@ -52,8 +51,9 @@ func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.Ali
return nil, err
}
return &AlistUserCacheData{
Host: v.Host,
Backend: v.Backend,
Host: v.Host,
ServerID: v.ServerID,
Backend: v.Backend,
}, nil
} else {
resp, err := cli.Login(ctx, &alist.LoginReq{
@ -67,9 +67,10 @@ func AlistAuthorizationCacheWithConfigInitFunc(ctx context.Context, v *model.Ali
}
return &AlistUserCacheData{
Host: v.Host,
Token: resp.Token,
Backend: v.Backend,
Host: v.Host,
ServerID: v.ServerID,
Token: resp.Token,
Backend: v.Backend,
}, nil
}
}
@ -141,7 +142,15 @@ func NewAlistMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, ar
if len(args) == 0 {
return nil, errors.New("need alist user cache")
}
aucd, err := args[0].Get(ctx)
var (
serverID string
err error
)
serverID, movie.Base.VendorInfo.Alist.Path, err = model.GetAlistServerIdFromPath(movie.Base.VendorInfo.Alist.Path)
if err != nil {
return nil, err
}
aucd, err := args[0].LoadOrStore(ctx, serverID)
if err != nil {
return nil, err
}

@ -7,7 +7,6 @@ import (
"io"
"net/http"
"net/url"
"strings"
"github.com/synctv-org/synctv/internal/db"
"github.com/synctv-org/synctv/internal/model"
@ -33,15 +32,10 @@ func NewEmbyUserCache(userID string) *EmbyUserCache {
}
func EmbyAuthorizationCacheWithUserIDInitFunc(userID, serverID string) (*EmbyUserCacheData, error) {
var (
v *model.EmbyVendor
err error
)
if serverID == "" {
v, err = db.GetEmbyFirstVendor(userID)
} else {
v, err = db.GetEmbyVendor(userID, serverID)
return nil, errors.New("serverID is required")
}
v, err := db.GetEmbyVendor(userID, serverID)
if err != nil {
return nil, err
}
@ -85,12 +79,13 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
return nil, errors.New("need emby user cache")
}
var serverID, itemID string
if s := strings.Split(movie.Base.VendorInfo.Emby.Path, "/"); len(s) == 2 {
serverID = s[0]
itemID = s[1]
} else {
return nil, errors.New("path is invalid")
var (
serverID string
err error
)
serverID, movie.Base.VendorInfo.Emby.Path, err = model.GetEmbyServerIdFromPath(movie.Base.VendorInfo.Emby.Path)
if err != nil {
return nil, err
}
aucd, err := args[0].LoadOrStore(ctx, serverID)
@ -108,7 +103,7 @@ func NewEmbyMovieCacheInitFunc(movie *model.Movie) func(ctx context.Context, arg
data, err := cli.GetItem(ctx, &emby.GetItemReq{
Host: aucd.Host,
Token: aucd.ApiKey,
ItemId: itemID,
ItemId: movie.Base.VendorInfo.Emby.Path,
})
if err != nil {
return nil, err

@ -32,19 +32,32 @@ func DeleteBilibiliVendor(userID string) error {
return db.Where("user_id = ?", userID).Delete(&model.BilibiliVendor{}).Error
}
func GetAlistVendor(userID string) (*model.AlistVendor, error) {
func GetAlistVendors(userID string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.AlistVendor, error) {
var vendors []*model.AlistVendor
err := db.Scopes(scopes...).Where("user_id = ?", userID).Find(&vendors).Error
return vendors, err
}
func GetAlistVendorsCount(userID string, scopes ...func(*gorm.DB) *gorm.DB) (int64, error) {
var count int64
err := db.Scopes(scopes...).Where("user_id = ?", userID).Model(&model.AlistVendor{}).Count(&count).Error
return count, err
}
func GetAlistVendor(userID, serverID string) (*model.AlistVendor, error) {
var vendor model.AlistVendor
err := db.Where("user_id = ?", userID).First(&vendor).Error
err := db.Where("user_id = ? AND server_id = ?", userID, serverID).First(&vendor).Error
return &vendor, HandleNotFound(err, "vendor")
}
func CreateOrSaveAlistVendor(vendorInfo *model.AlistVendor) (*model.AlistVendor, error) {
if vendorInfo.UserID == "" {
return nil, errors.New("user_id must not be empty")
if vendorInfo.UserID == "" || vendorInfo.ServerID == "" {
return nil, errors.New("user_id and server_id must not be empty")
}
return vendorInfo, Transactional(func(tx *gorm.DB) error {
if errors.Is(tx.First(&model.AlistVendor{
UserID: vendorInfo.UserID,
UserID: vendorInfo.UserID,
ServerID: vendorInfo.ServerID,
}).Error, gorm.ErrRecordNotFound) {
return tx.Create(&vendorInfo).Error
} else {
@ -53,8 +66,8 @@ func CreateOrSaveAlistVendor(vendorInfo *model.AlistVendor) (*model.AlistVendor,
})
}
func DeleteAlistVendor(userID string) error {
return db.Where("user_id = ?", userID).Delete(&model.AlistVendor{}).Error
func DeleteAlistVendor(userID, serverID string) error {
return db.Where("user_id = ? AND server_id = ?", userID, serverID).Delete(&model.AlistVendor{}).Error
}
func GetEmbyVendors(userID string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.EmbyVendor, error) {

@ -2,6 +2,7 @@ package model
import (
"fmt"
"strings"
"time"
"github.com/synctv-org/synctv/utils"
@ -85,10 +86,19 @@ func (b *BilibiliStreamingInfo) Validate() error {
}
type AlistStreamingInfo struct {
// {/}serverId/Path
Path string `gorm:"type:varchar(4096)" json:"path,omitempty"`
Password string `gorm:"type:varchar(256)" json:"password,omitempty"`
}
func GetAlistServerIdFromPath(path string) (serverID string, filePath string, err error) {
before, after, found := strings.Cut(strings.TrimLeft(path, "/"), "/")
if !found {
return "", path, fmt.Errorf("path is invalid")
}
return before, after, nil
}
func (a *AlistStreamingInfo) Validate() error {
if a.Path == "" {
return fmt.Errorf("path is empty")
@ -123,9 +133,17 @@ func (a *AlistStreamingInfo) AfterFind(tx *gorm.DB) error {
}
type EmbyStreamingInfo struct {
// {/}serverId/ItemId
Path string `gorm:"type:varchar(52)" json:"path,omitempty"`
}
func GetEmbyServerIdFromPath(path string) (serverID string, filePath string, err error) {
if s := strings.Split(strings.TrimLeft(path, "/"), "/"); len(s) == 2 {
return s[0], s[1], nil
}
return "", path, fmt.Errorf("path is invalid")
}
func (e *EmbyStreamingInfo) Validate() error {
if e.Path == "" {
return fmt.Errorf("path is empty")

@ -51,7 +51,7 @@ type User struct {
Rooms []Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
AlistVendor *AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
AlistVendor []*AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}

@ -3,6 +3,7 @@ package model
import (
"time"
"github.com/google/uuid"
"github.com/synctv-org/synctv/utils"
"gorm.io/gorm"
)
@ -48,11 +49,18 @@ type AlistVendor struct {
UpdatedAt time.Time
UserID string `gorm:"primaryKey;type:char(32)"`
Backend string `gorm:"type:varchar(64)"`
ServerID string `gorm:"primaryKey;type:char(32)"`
Host string `gorm:"not null;type:varchar(256)"`
Username string `gorm:"type:varchar(256)"`
HashedPassword []byte
}
func GenAlistServerID(a *AlistVendor) {
if a.ServerID == "" {
a.ServerID = utils.SortUUIDWithUUID(uuid.NewMD5(uuid.NameSpaceURL, []byte(a.Host)))
}
}
func (a *AlistVendor) BeforeSave(tx *gorm.DB) error {
key := utils.GenCryptoKey(a.UserID)
var err error

@ -245,6 +245,8 @@ func initVendor(vendor *gin.RouterGroup) {
alist.POST("/list", vendorAlist.List)
alist.GET("/me", vendorAlist.Me)
alist.GET("/binds", vendorAlist.Binds)
}
{

@ -9,20 +9,30 @@ import (
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
"github.com/synctv-org/synctv/internal/db"
dbModel "github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/internal/op"
"github.com/synctv-org/synctv/internal/vendor"
"github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils"
"github.com/synctv-org/vendors/api/alist"
"gorm.io/gorm"
)
type ListReq struct {
ServerID string `json:"-"`
Path string `json:"path"`
Password string `json:"password"`
Refresh bool `json:"refresh"`
}
func (r *ListReq) Validate() error {
func (r *ListReq) Validate() (err error) {
if r.Path == "" {
return nil
}
r.ServerID, r.Path, err = dbModel.GetAlistServerIdFromPath(r.Path)
if err != nil {
return err
}
if r.Path == "" {
r.Path = "/"
}
@ -50,14 +60,65 @@ func List(ctx *gin.Context) {
return
}
page, size, err := utils.GetPageAndMax(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if req.ServerID == "" {
socpes := [](func(*gorm.DB) *gorm.DB){
db.OrderByCreatedAtAsc,
}
ev, err := db.GetAlistVendors(user.ID, append(socpes, db.Paginate(page, size))...)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist server id not found"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
total, err := db.GetAlistVendorsCount(user.ID, socpes...)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
resp := AlistFSListResp{
Paths: []*model.Path{
{
Name: "",
Path: "",
},
},
Total: uint64(total),
}
for _, evi := range ev {
resp.Items = append(resp.Items, &AlistFileItem{
Item: &model.Item{
Name: evi.Host,
Path: evi.ServerID + `/`,
IsDir: true,
},
})
}
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
return
}
if !strings.HasPrefix(req.Path, "/") {
req.Path = "/" + req.Path
}
aucd, err := user.AlistCache().Get(ctx)
aucd, err := user.AlistCache().LoadOrStore(ctx, req.ServerID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist not login"))
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist server id not found"))
return
}
@ -65,12 +126,6 @@ func List(ctx *gin.Context) {
return
}
page, size, err := utils.GetPageAndMax(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
var cli = vendor.LoadAlistClient(ctx.Query("backend"))
data, err := cli.FsList(ctx, &alist.FsListReq{
Token: aucd.Token,
@ -86,16 +141,24 @@ func List(ctx *gin.Context) {
return
}
req.Path = strings.TrimRight(req.Path, "/")
req.Path = strings.Trim(req.Path, "/")
resp := AlistFSListResp{
Total: data.Total,
Paths: model.GenDefaultPaths(req.Path),
Paths: model.GenDefaultPaths(req.Path, true,
&model.Path{
Name: "",
Path: "",
},
&model.Path{
Name: aucd.Host,
Path: aucd.ServerID + "/",
}),
}
for _, flr := range data.Content {
resp.Items = append(resp.Items, &AlistFileItem{
Item: &model.Item{
Name: flr.Name,
Path: fmt.Sprintf("%s/%s", req.Path, flr.Name),
Path: fmt.Sprintf("%s/%s", aucd.ServerID, strings.Trim(fmt.Sprintf("%s/%s", req.Path, flr.Name), "/")),
IsDir: flr.IsDir,
},
Size: flr.Size,

@ -6,6 +6,8 @@ import (
"encoding/hex"
"errors"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
@ -27,6 +29,14 @@ func (r *LoginReq) Validate() error {
if r.Host == "" {
return errors.New("host is required")
}
url, err := url.Parse(r.Host)
if err != nil {
return err
}
if url.Scheme != "http" && url.Scheme != "https" {
return errors.New("host is invalid")
}
r.Host = strings.TrimRight(url.String(), "/")
if r.Password != "" && r.HashedPassword != "" {
return errors.New("password and hashedPassword can't be both set")
}
@ -67,8 +77,9 @@ func Login(ctx *gin.Context) {
_, err = db.CreateOrSaveAlistVendor(&dbModel.AlistVendor{
UserID: user.ID,
Backend: backend,
Host: req.Host,
ServerID: data.ServerID,
Backend: data.Backend,
Host: data.Host,
Username: req.Username,
HashedPassword: []byte(req.HashedPassword),
})
@ -77,7 +88,7 @@ func Login(ctx *gin.Context) {
return
}
_, err = user.AlistCache().Data().Refresh(ctx, func(ctx context.Context, args ...struct{}) (*cache.AlistUserCacheData, error) {
_, err = user.AlistCache().StoreOrRefreshWithDynamicFunc(ctx, data.ServerID, func(ctx context.Context, key string, args ...struct{}) (*cache.AlistUserCacheData, error) {
return data, nil
})
if err != nil {
@ -91,13 +102,21 @@ func Login(ctx *gin.Context) {
func Logout(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
err := db.DeleteAlistVendor(user.ID)
var req model.ServerIDReq
if err := model.Decode(ctx, &req); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
err := db.DeleteAlistVendor(user.ID, req.ServerID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
user.AlistCache().Clear()
if rc, ok := user.AlistCache().LoadCache(req.ServerID); ok {
rc.Clear()
}
ctx.Status(http.StatusNoContent)
}

@ -17,12 +17,17 @@ type AlistMeResp = model.VendorMeResp[*alist.MeResp]
func Me(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
aucd, err := user.AlistCache().Get(ctx)
serverID := ctx.Query("serverID")
if serverID == "" {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(errors.New("serverID is required")))
return
}
aucd, err := user.AlistCache().LoadOrStore(ctx, serverID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusOK, model.NewApiDataResp(&AlistMeResp{
IsLogin: false,
}))
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("alist server id not found"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
@ -43,3 +48,37 @@ func Me(ctx *gin.Context) {
Info: resp,
}))
}
type AlistBindsResp []*struct {
ServerID string `json:"serverID"`
Host string `json:"host"`
}
func Binds(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
ev, err := db.GetAlistVendors(user.ID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusOK, model.NewApiDataResp(&AlistMeResp{
IsLogin: false,
}))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
var resp AlistBindsResp = make(AlistBindsResp, len(ev))
for i, v := range ev {
resp[i] = &struct {
ServerID string "json:\"serverID\""
Host string "json:\"host\""
}{
ServerID: v.ServerID,
Host: v.Host,
}
}
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
}

@ -5,11 +5,11 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
"github.com/synctv-org/synctv/internal/db"
dbModel "github.com/synctv-org/synctv/internal/model"
"github.com/synctv-org/synctv/internal/op"
"github.com/synctv-org/synctv/internal/vendor"
"github.com/synctv-org/synctv/server/model"
@ -24,10 +24,13 @@ type ListReq struct {
Keywords string `json:"keywords"`
}
func (r *ListReq) Validate() error {
if s := strings.Split(r.Path, "/"); len(s) == 2 {
r.ServerID = s[0]
r.Path = s[1]
func (r *ListReq) Validate() (err error) {
if r.Path == "" {
return nil
}
r.ServerID, r.Path, err = dbModel.GetEmbyServerIdFromPath(r.Path)
if err != nil {
return err
}
if r.Path == "" {
return nil
@ -79,7 +82,7 @@ func List(ctx *gin.Context) {
ev, err := db.GetEmbyVendors(user.ID, append(socpes, db.Paginate(page, size))...)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby not login"))
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby server id not found"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
@ -121,7 +124,7 @@ func List(ctx *gin.Context) {
aucd, err := user.EmbyCache().LoadOrStore(ctx, req.ServerID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby not login"))
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby server id not found"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))

@ -4,6 +4,8 @@ import (
"context"
"errors"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
@ -27,6 +29,14 @@ func (r *LoginReq) Validate() error {
if r.Host == "" {
return errors.New("host is required")
}
url, err := url.Parse(r.Host)
if err != nil {
return err
}
if url.Scheme != "http" && url.Scheme != "https" {
return errors.New("host is invalid")
}
r.Host = strings.TrimRight(url.String(), "/")
if r.ApiKey == "" && (r.Username == "" || r.Password == "") {
return errors.New("username and password or apiKey is required")
}
@ -108,25 +118,10 @@ func Login(ctx *gin.Context) {
ctx.Status(http.StatusNoContent)
}
type LogoutReq struct {
ServerID string `json:"serverId"`
}
func (r *LogoutReq) Validate() error {
if r.ServerID == "" {
return errors.New("serverId is required")
}
return nil
}
func (r *LogoutReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(r)
}
func Logout(ctx *gin.Context) {
user := ctx.MustGet("user").(*op.User)
var req LogoutReq
var req model.ServerIDReq
if err := model.Decode(ctx, &req); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return

@ -26,6 +26,10 @@ func Me(ctx *gin.Context) {
eucd, err := user.EmbyCache().LoadOrStore(ctx, serverID)
if err != nil {
if errors.Is(err, db.ErrNotFound("vendor")) {
ctx.JSON(http.StatusBadRequest, model.NewApiErrorStringResp("emby server id not found"))
return
}
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}

@ -1,8 +1,12 @@
package model
import (
"errors"
"fmt"
"strings"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
)
type VendorMeResp[T any] struct {
@ -16,14 +20,16 @@ type VendorFSListResp[T any] struct {
Total uint64 `json:"total"`
}
func GenDefaultPaths(path string) []*Path {
paths := []*Path{}
func GenDefaultPaths(path string, skipEmpty bool, paths ...*Path) []*Path {
path = strings.TrimRight(path, "/")
for i, v := range strings.Split(path, `/`) {
if i != 0 {
for _, v := range strings.Split(path, `/`) {
if skipEmpty && v == "" {
continue
}
if l := len(paths); l != 0 {
paths = append(paths, &Path{
Name: v,
Path: fmt.Sprintf("%s/%s", paths[i-1].Path, v),
Path: fmt.Sprintf("%s/%s", strings.TrimRight(paths[l-1].Path, "/"), v),
})
} else {
paths = append(paths, &Path{
@ -45,3 +51,18 @@ type Item struct {
Path string `json:"path"`
IsDir bool `json:"isDir"`
}
type ServerIDReq struct {
ServerID string `json:"serverId"`
}
func (r *ServerIDReq) Validate() error {
if r.ServerID == "" {
return errors.New("serverId is required")
}
return nil
}
func (r *ServerIDReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(r)
}

@ -310,7 +310,10 @@ func LIKE(s string) string {
}
func SortUUID() string {
src := uuid.New()
return SortUUIDWithUUID(uuid.New())
}
func SortUUIDWithUUID(src uuid.UUID) string {
dst := make([]byte, 32)
hex.Encode(dst, src[:])
return stream.BytesToString(dst)

Loading…
Cancel
Save