chore: 初始化 Cloudreve 源码仓库
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/node"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/task"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader/qbittorrent"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader/slave"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
Node interface {
|
||||
fs.StatelessUploadManager
|
||||
ID() int
|
||||
Name() string
|
||||
IsMaster() bool
|
||||
// CreateTask creates a task on the node. It does not have effect on master node.
|
||||
CreateTask(ctx context.Context, taskType string, state string) (int, error)
|
||||
// GetTask returns the task summary of the task with the given id.
|
||||
GetTask(ctx context.Context, id int, clearOnComplete bool) (*SlaveTaskSummary, error)
|
||||
// CleanupFolders cleans up the given folders on the node.
|
||||
CleanupFolders(ctx context.Context, folders ...string) error
|
||||
// AuthInstance returns the auth instance for the node.
|
||||
AuthInstance() auth.Auth
|
||||
// CreateDownloader creates a downloader instance from the node for remote download tasks.
|
||||
CreateDownloader(ctx context.Context, c request.Client, settings setting.Provider) (downloader.Downloader, error)
|
||||
// Settings returns the settings of the node.
|
||||
Settings(ctx context.Context) *types.NodeSetting
|
||||
}
|
||||
|
||||
// Request body for creating tasks on slave node
|
||||
CreateSlaveTask struct {
|
||||
Type string `json:"type"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// Request body for cleaning up folders on slave node
|
||||
FolderCleanup struct {
|
||||
Path []string `json:"path" binding:"required"`
|
||||
}
|
||||
|
||||
SlaveTaskSummary struct {
|
||||
Status task.Status `json:"status"`
|
||||
Error string `json:"error"`
|
||||
PrivateState string `json:"private_state"`
|
||||
Progress queue.Progresses `json:"progress,omitempty"`
|
||||
}
|
||||
|
||||
MasterSiteUrlCtx struct{}
|
||||
MasterSiteVersionCtx struct{}
|
||||
MasterSiteIDCtx struct{}
|
||||
SlaveNodeIDCtx struct{}
|
||||
masterNode struct {
|
||||
nodeBase
|
||||
client request.Client
|
||||
}
|
||||
)
|
||||
|
||||
func newNode(ctx context.Context, model *ent.Node, config conf.ConfigProvider, settings setting.Provider) Node {
|
||||
if model.Type == node.TypeMaster {
|
||||
return newMasterNode(model, config, settings)
|
||||
}
|
||||
return newSlaveNode(ctx, model, config, settings)
|
||||
}
|
||||
|
||||
func newMasterNode(model *ent.Node, config conf.ConfigProvider, settings setting.Provider) *masterNode {
|
||||
n := &masterNode{
|
||||
nodeBase: nodeBase{
|
||||
model: model,
|
||||
},
|
||||
}
|
||||
|
||||
if config.System().Mode == conf.SlaveMode {
|
||||
n.client = request.NewClient(config,
|
||||
request.WithCorrelationID(),
|
||||
request.WithCredential(auth.HMACAuth{
|
||||
[]byte(config.Slave().Secret),
|
||||
}, int64(config.Slave().SignatureTTL)),
|
||||
)
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (b *masterNode) PrepareUpload(ctx context.Context, args *fs.StatelessPrepareUploadService) (*fs.StatelessPrepareUploadResponse, error) {
|
||||
reqBody, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
requestDst := routes.MasterStatelessUrl(MasterSiteUrlFromContext(ctx), "prepare")
|
||||
resp, err := b.client.Request(
|
||||
"PUT",
|
||||
requestDst.String(),
|
||||
bytes.NewReader(reqBody),
|
||||
request.WithContext(ctx),
|
||||
request.WithSlaveMeta(NodeIdFromContext(ctx)),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return nil, serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
uploadRequest := &fs.StatelessPrepareUploadResponse{}
|
||||
resp.GobDecode(uploadRequest)
|
||||
|
||||
return uploadRequest, nil
|
||||
}
|
||||
|
||||
func (b *masterNode) CompleteUpload(ctx context.Context, args *fs.StatelessCompleteUploadService) error {
|
||||
reqBody, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
requestDst := routes.MasterStatelessUrl(MasterSiteUrlFromContext(ctx), "complete")
|
||||
resp, err := b.client.Request(
|
||||
"POST",
|
||||
requestDst.String(),
|
||||
bytes.NewReader(reqBody),
|
||||
request.WithContext(ctx),
|
||||
request.WithSlaveMeta(NodeIdFromContext(ctx)),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *masterNode) OnUploadFailed(ctx context.Context, args *fs.StatelessOnUploadFailedService) error {
|
||||
reqBody, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
requestDst := routes.MasterStatelessUrl(MasterSiteUrlFromContext(ctx), "failed")
|
||||
resp, err := b.client.Request(
|
||||
"POST",
|
||||
requestDst.String(),
|
||||
bytes.NewReader(reqBody),
|
||||
request.WithContext(ctx),
|
||||
request.WithSlaveMeta(NodeIdFromContext(ctx)),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *masterNode) CreateFile(ctx context.Context, args *fs.StatelessCreateFileService) error {
|
||||
reqBody, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
requestDst := routes.MasterStatelessUrl(MasterSiteUrlFromContext(ctx), "create")
|
||||
resp, err := b.client.Request(
|
||||
"POST",
|
||||
requestDst.String(),
|
||||
bytes.NewReader(reqBody),
|
||||
request.WithContext(ctx),
|
||||
request.WithSlaveMeta(NodeIdFromContext(ctx)),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (b *masterNode) CreateDownloader(ctx context.Context, c request.Client, settings setting.Provider) (downloader.Downloader, error) {
|
||||
return NewDownloader(ctx, c, settings, b.Settings(ctx))
|
||||
}
|
||||
|
||||
// NewDownloader creates a new downloader instance from the node for remote download tasks.
|
||||
func NewDownloader(ctx context.Context, c request.Client, settings setting.Provider, options *types.NodeSetting) (downloader.Downloader, error) {
|
||||
if options.Provider == types.DownloaderProviderQBittorrent {
|
||||
return qbittorrent.NewClient(logging.FromContext(ctx), c, settings, options.QBittorrentSetting)
|
||||
} else if options.Provider == types.DownloaderProviderAria2 {
|
||||
return aria2.New(logging.FromContext(ctx), settings, options.Aria2Setting), nil
|
||||
} else if options.Provider == "" {
|
||||
return nil, errors.New("downloader not configured for this node")
|
||||
} else {
|
||||
return nil, errors.New("unknown downloader provider")
|
||||
}
|
||||
}
|
||||
|
||||
type slaveNode struct {
|
||||
nodeBase
|
||||
client request.Client
|
||||
}
|
||||
|
||||
func newSlaveNode(ctx context.Context, model *ent.Node, config conf.ConfigProvider, settings setting.Provider) *slaveNode {
|
||||
siteBasic := settings.SiteBasic(ctx)
|
||||
return &slaveNode{
|
||||
nodeBase: nodeBase{
|
||||
model: model,
|
||||
},
|
||||
client: request.NewClient(config,
|
||||
request.WithCorrelationID(),
|
||||
request.WithSlaveMeta(model.ID),
|
||||
request.WithMasterMeta(siteBasic.ID, settings.SiteURL(setting.UseFirstSiteUrl(ctx)).String()),
|
||||
request.WithCredential(auth.HMACAuth{[]byte(model.SlaveKey)}, int64(settings.SlaveRequestSignTTL(ctx))),
|
||||
request.WithEndpoint(model.Server)),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *slaveNode) CreateTask(ctx context.Context, taskType string, state string) (int, error) {
|
||||
reqBody, err := json.Marshal(&CreateSlaveTask{
|
||||
Type: taskType,
|
||||
State: state,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
resp, err := n.client.Request(
|
||||
"PUT",
|
||||
constants.APIPrefixSlave+"/task",
|
||||
bytes.NewReader(reqBody),
|
||||
request.WithContext(ctx),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return 0, serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
taskId := 0
|
||||
if resp.GobDecode(&taskId); taskId > 0 {
|
||||
return taskId, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unexpected response data: %v", resp.Data)
|
||||
}
|
||||
|
||||
func (n *slaveNode) GetTask(ctx context.Context, id int, clearOnComplete bool) (*SlaveTaskSummary, error) {
|
||||
resp, err := n.client.Request(
|
||||
"GET",
|
||||
routes.SlaveGetTaskRoute(id, clearOnComplete),
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return nil, serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
summary := &SlaveTaskSummary{}
|
||||
resp.GobDecode(summary)
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func (b *slaveNode) CleanupFolders(ctx context.Context, folders ...string) error {
|
||||
args := &FolderCleanup{
|
||||
Path: folders,
|
||||
}
|
||||
reqBody, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
resp, err := b.client.Request(
|
||||
"POST",
|
||||
constants.APIPrefixSlave+"/task/cleanup",
|
||||
bytes.NewReader(reqBody),
|
||||
request.WithContext(ctx),
|
||||
request.WithLogger(logging.FromContext(ctx)),
|
||||
).CheckHTTPResponse(200).DecodeResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
if resp.Code != 0 {
|
||||
return serializer.NewErrorFromResponse(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *slaveNode) CreateDownloader(ctx context.Context, c request.Client, settings setting.Provider) (downloader.Downloader, error) {
|
||||
return slave.NewSlaveDownloader(b.client, b.Settings(ctx)), nil
|
||||
}
|
||||
|
||||
type nodeBase struct {
|
||||
model *ent.Node
|
||||
}
|
||||
|
||||
func (b *nodeBase) ID() int {
|
||||
return b.model.ID
|
||||
}
|
||||
|
||||
func (b *nodeBase) Name() string {
|
||||
return b.model.Name
|
||||
}
|
||||
|
||||
func (b *nodeBase) IsMaster() bool {
|
||||
return b.model.Type == node.TypeMaster
|
||||
}
|
||||
|
||||
func (b *nodeBase) CreateTask(ctx context.Context, taskType string, state string) (int, error) {
|
||||
return 0, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) AuthInstance() auth.Auth {
|
||||
return auth.HMACAuth{[]byte(b.model.SlaveKey)}
|
||||
}
|
||||
|
||||
func (b *nodeBase) GetTask(ctx context.Context, id int, clearOnComplete bool) (*SlaveTaskSummary, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) CleanupFolders(ctx context.Context, folders ...string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) PrepareUpload(ctx context.Context, args *fs.StatelessPrepareUploadService) (*fs.StatelessPrepareUploadResponse, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) CompleteUpload(ctx context.Context, args *fs.StatelessCompleteUploadService) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) OnUploadFailed(ctx context.Context, args *fs.StatelessOnUploadFailedService) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) CreateFile(ctx context.Context, args *fs.StatelessCreateFileService) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) CreateDownloader(ctx context.Context, c request.Client, settings setting.Provider) (downloader.Downloader, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (b *nodeBase) Settings(ctx context.Context) *types.NodeSetting {
|
||||
return b.model.Settings
|
||||
}
|
||||
|
||||
func NodeIdFromContext(ctx context.Context) int {
|
||||
nodeIdStr, ok := ctx.Value(SlaveNodeIDCtx{}).(string)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
nodeId, _ := strconv.Atoi(nodeIdStr)
|
||||
return nodeId
|
||||
}
|
||||
|
||||
func MasterSiteUrlFromContext(ctx context.Context) string {
|
||||
if u, ok := ctx.Value(MasterSiteUrlCtx{}).(string); ok {
|
||||
return u
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/node"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type NodePool interface {
|
||||
// Upsert updates or inserts a node into the pool.
|
||||
Upsert(ctx context.Context, node *ent.Node)
|
||||
// Get returns a node with the given capability and preferred node id. `allowed` is a list of allowed node ids.
|
||||
// If `allowed` is empty, all nodes with the capability are considered.
|
||||
Get(ctx context.Context, capability types.NodeCapability, preferred int) (Node, error)
|
||||
}
|
||||
|
||||
type (
|
||||
weightedNodePool struct {
|
||||
lock sync.RWMutex
|
||||
|
||||
conf conf.ConfigProvider
|
||||
settings setting.Provider
|
||||
|
||||
nodes map[types.NodeCapability][]*nodeItem
|
||||
}
|
||||
|
||||
nodeItem struct {
|
||||
node Node
|
||||
weight int
|
||||
current int
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAvailableNode = fmt.Errorf("no available node found")
|
||||
|
||||
supportedCapabilities = []types.NodeCapability{
|
||||
types.NodeCapabilityNone,
|
||||
types.NodeCapabilityCreateArchive,
|
||||
types.NodeCapabilityExtractArchive,
|
||||
types.NodeCapabilityRemoteDownload,
|
||||
}
|
||||
)
|
||||
|
||||
func NewNodePool(ctx context.Context, l logging.Logger, config conf.ConfigProvider, settings setting.Provider,
|
||||
client inventory.NodeClient) (NodePool, error) {
|
||||
nodes, err := client.ListActiveNodes(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list active nodes: %w", err)
|
||||
}
|
||||
|
||||
pool := &weightedNodePool{
|
||||
nodes: make(map[types.NodeCapability][]*nodeItem),
|
||||
conf: config,
|
||||
settings: settings,
|
||||
}
|
||||
for _, node := range nodes {
|
||||
for _, capability := range supportedCapabilities {
|
||||
// If current capability is enabled, add it to pool slot.
|
||||
if capability == types.NodeCapabilityNone ||
|
||||
(node.Capabilities != nil && node.Capabilities.Enabled(int(capability))) {
|
||||
if _, ok := pool.nodes[capability]; !ok {
|
||||
pool.nodes[capability] = make([]*nodeItem, 0)
|
||||
}
|
||||
|
||||
l.Debug("Add node %q to capability slot %d with weight %d", node.Name, capability, node.Weight)
|
||||
pool.nodes[capability] = append(pool.nodes[capability], &nodeItem{
|
||||
node: newNode(ctx, node, config, settings),
|
||||
weight: node.Weight,
|
||||
current: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func (p *weightedNodePool) Get(ctx context.Context, capability types.NodeCapability, preferred int) (Node, error) {
|
||||
l := logging.FromContext(ctx)
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
nodes, ok := p.nodes[capability]
|
||||
if !ok || len(nodes) == 0 {
|
||||
return nil, fmt.Errorf("no node found with capability %d: %w", capability, ErrNoAvailableNode)
|
||||
}
|
||||
|
||||
var selected *nodeItem
|
||||
|
||||
if preferred > 0 {
|
||||
// First try to find the preferred node.
|
||||
for _, n := range nodes {
|
||||
if n.node.ID() == preferred {
|
||||
selected = n
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selected == nil {
|
||||
l.Debug("Preferred node %d not found, fallback to select a node with the least current weight", preferred)
|
||||
}
|
||||
}
|
||||
|
||||
if selected == nil {
|
||||
// If no preferred one, or the preferred one is not available, select a node with the least current weight.
|
||||
|
||||
// Total weight of all items.
|
||||
var total int
|
||||
|
||||
// Loop through the list of items and add the item's weight to the current weight.
|
||||
// Also increment the total weight counter.
|
||||
var maxNode *nodeItem
|
||||
for _, item := range nodes {
|
||||
item.current += max(1, item.weight)
|
||||
total += max(1, item.weight)
|
||||
|
||||
// Select the item with max weight.
|
||||
if maxNode == nil || item.current > maxNode.current {
|
||||
maxNode = item
|
||||
}
|
||||
}
|
||||
|
||||
// Select the item with the max weight.
|
||||
selected = maxNode
|
||||
if selected == nil {
|
||||
return nil, fmt.Errorf("no node found with capability %d: %w", capability, ErrNoAvailableNode)
|
||||
}
|
||||
|
||||
l.Debug("Selected node %q with weight=%d, current=%d, total=%d", selected.node.Name(), selected.weight, maxNode.current, total)
|
||||
|
||||
// Reduce the current weight of the selected item by the total weight.
|
||||
maxNode.current -= total
|
||||
}
|
||||
|
||||
return selected.node, nil
|
||||
}
|
||||
|
||||
func (p *weightedNodePool) Upsert(ctx context.Context, n *ent.Node) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
for _, capability := range supportedCapabilities {
|
||||
_, index, found := lo.FindIndexOf(p.nodes[capability], func(i *nodeItem) bool {
|
||||
return i.node.ID() == n.ID
|
||||
})
|
||||
if capability == types.NodeCapabilityNone ||
|
||||
(n.Capabilities != nil && n.Capabilities.Enabled(int(capability))) {
|
||||
if n.Status != node.StatusActive && found {
|
||||
// Remove inactive node
|
||||
p.nodes[capability] = append(p.nodes[capability][:index], p.nodes[capability][index+1:]...)
|
||||
continue
|
||||
}
|
||||
|
||||
if found {
|
||||
p.nodes[capability][index].node = newNode(ctx, n, p.conf, p.settings)
|
||||
} else {
|
||||
p.nodes[capability] = append(p.nodes[capability], &nodeItem{
|
||||
node: newNode(ctx, n, p.conf, p.settings),
|
||||
weight: n.Weight,
|
||||
current: 0,
|
||||
})
|
||||
}
|
||||
} else if found {
|
||||
// Capability changed, remove the old node.
|
||||
p.nodes[capability] = append(p.nodes[capability][:index], p.nodes[capability][index+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type slaveDummyNodePool struct {
|
||||
conf conf.ConfigProvider
|
||||
settings setting.Provider
|
||||
masterNode Node
|
||||
}
|
||||
|
||||
func NewSlaveDummyNodePool(ctx context.Context, config conf.ConfigProvider, settings setting.Provider) NodePool {
|
||||
return &slaveDummyNodePool{
|
||||
conf: config,
|
||||
settings: settings,
|
||||
masterNode: newNode(ctx, &ent.Node{
|
||||
ID: 0,
|
||||
Name: "Master",
|
||||
Type: node.TypeMaster,
|
||||
}, config, settings),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slaveDummyNodePool) Upsert(ctx context.Context, node *ent.Node) {
|
||||
}
|
||||
|
||||
func (s *slaveDummyNodePool) Get(ctx context.Context, capability types.NodeCapability, preferred int) (Node, error) {
|
||||
return s.masterNode, nil
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
IsDownloadQuery = "download"
|
||||
IsThumbQuery = "thumb"
|
||||
SlaveClearTaskRegistryQuery = "deleteOnComplete"
|
||||
)
|
||||
|
||||
var (
|
||||
masterPing *url.URL
|
||||
masterUserActivate *url.URL
|
||||
masterUserReset *url.URL
|
||||
masterHome *url.URL
|
||||
)
|
||||
|
||||
func init() {
|
||||
masterPing, _ = url.Parse(constants.APIPrefix + "/site/ping")
|
||||
masterUserActivate, _ = url.Parse("/session/activate")
|
||||
masterUserReset, _ = url.Parse("/session/reset")
|
||||
}
|
||||
|
||||
func FrontendHomeUrl(base *url.URL, path string) *url.URL {
|
||||
route, _ := url.Parse(fmt.Sprintf("/home"))
|
||||
q := route.Query()
|
||||
q.Set("path", path)
|
||||
route.RawQuery = q.Encode()
|
||||
|
||||
return base.ResolveReference(route)
|
||||
}
|
||||
|
||||
func MasterPingUrl(base *url.URL) *url.URL {
|
||||
return base.ResolveReference(masterPing)
|
||||
}
|
||||
|
||||
func MasterSlaveCallbackUrl(base *url.URL, driver, id, secret string) *url.URL {
|
||||
apiBaseURI, _ := url.Parse(path.Join(constants.APIPrefix+"/callback", driver, id, secret))
|
||||
return base.ResolveReference(apiBaseURI)
|
||||
}
|
||||
|
||||
func MasterUserActivateAPIUrl(base *url.URL, uid string) *url.URL {
|
||||
route, _ := url.Parse(constants.APIPrefix + "/user/activate/" + uid)
|
||||
return base.ResolveReference(route)
|
||||
}
|
||||
|
||||
func MasterUserActivateUrl(base *url.URL) *url.URL {
|
||||
return base.ResolveReference(masterUserActivate)
|
||||
}
|
||||
|
||||
func MasterUserResetUrl(base *url.URL) *url.URL {
|
||||
return base.ResolveReference(masterUserReset)
|
||||
}
|
||||
|
||||
func MasterUserAvatarUrl(base *url.URL, uid string) *url.URL {
|
||||
route, _ := url.Parse(constants.APIPrefix + "/user/avatar/" + uid)
|
||||
return base.ResolveReference(route)
|
||||
}
|
||||
|
||||
func MasterShareUrl(base *url.URL, id, password string) *url.URL {
|
||||
p := "/s/" + id
|
||||
if password != "" {
|
||||
p += ("/" + password)
|
||||
}
|
||||
route, _ := url.Parse(p)
|
||||
return base.ResolveReference(route)
|
||||
}
|
||||
|
||||
func MasterDirectLink(base *url.URL, id, name string) *url.URL {
|
||||
p := path.Join("/f", id, url.PathEscape(name))
|
||||
route, _ := url.Parse(p)
|
||||
return base.ResolveReference(route)
|
||||
}
|
||||
|
||||
// MasterShareLongUrl generates a long share URL for redirect.
|
||||
func MasterShareLongUrl(id, password string) *url.URL {
|
||||
base, _ := url.Parse("/home")
|
||||
q := base.Query()
|
||||
|
||||
q.Set("path", fs.NewShareUri(id, password))
|
||||
base.RawQuery = q.Encode()
|
||||
return base
|
||||
}
|
||||
|
||||
func MasterArchiveDownloadUrl(base *url.URL, sessionID string) *url.URL {
|
||||
routes, err := url.Parse(path.Join(constants.APIPrefix, "file", "archive", sessionID, "archive.zip"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return base.ResolveReference(routes)
|
||||
}
|
||||
|
||||
func MasterPolicyOAuthCallback(base *url.URL) *url.URL {
|
||||
if base.Scheme != "https" {
|
||||
base.Scheme = "https"
|
||||
}
|
||||
routes, err := url.Parse("/admin/policy/oauth")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return base.ResolveReference(routes)
|
||||
}
|
||||
|
||||
func MasterGetCredentialUrl(base, key string) *url.URL {
|
||||
masterBase, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
routes, err := url.Parse(path.Join(constants.APIPrefixSlave, "credential", key))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return masterBase.ResolveReference(routes)
|
||||
}
|
||||
|
||||
func MasterStatelessUrl(base, method string) *url.URL {
|
||||
masterBase, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
routes, err := url.Parse(path.Join(constants.APIPrefixSlave, "statelessUpload", method))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return masterBase.ResolveReference(routes)
|
||||
}
|
||||
|
||||
func SlaveUploadUrl(base *url.URL, sessionID string) *url.URL {
|
||||
base.Path = path.Join(base.Path, constants.APIPrefixSlave, "/upload", sessionID)
|
||||
return base
|
||||
}
|
||||
|
||||
func MasterFileContentUrl(base *url.URL, entityId, name string, download, thumb bool, speed int64) *url.URL {
|
||||
name = url.PathEscape(name)
|
||||
|
||||
route, _ := url.Parse(constants.APIPrefix + fmt.Sprintf("/file/content/%s/%d/%s", entityId, speed, name))
|
||||
if base != nil {
|
||||
route = base.ResolveReference(route)
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
if download {
|
||||
values.Set(IsDownloadQuery, "true")
|
||||
}
|
||||
|
||||
if thumb {
|
||||
values.Set(IsThumbQuery, "true")
|
||||
}
|
||||
|
||||
route.RawQuery = values.Encode()
|
||||
return route
|
||||
}
|
||||
|
||||
func MasterWopiSrc(base *url.URL, sessionId string) *url.URL {
|
||||
route, _ := url.Parse(constants.APIPrefix + "/file/wopi/" + sessionId)
|
||||
return base.ResolveReference(route)
|
||||
}
|
||||
|
||||
func SlaveFileContentUrl(base *url.URL, srcPath, name string, download bool, speed int64, nodeId int) *url.URL {
|
||||
srcPath = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(srcPath)))
|
||||
name = url.PathEscape(name)
|
||||
route, _ := url.Parse(constants.APIPrefixSlave + fmt.Sprintf("/file/content/%d/%s/%d/%s", nodeId, srcPath, speed, name))
|
||||
base = base.ResolveReference(route)
|
||||
|
||||
values := url.Values{}
|
||||
if download {
|
||||
values.Set(IsDownloadQuery, "true")
|
||||
}
|
||||
|
||||
base.RawQuery = values.Encode()
|
||||
return base
|
||||
}
|
||||
|
||||
func SlaveMediaMetaRoute(src, ext, language string) string {
|
||||
src = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(src)))
|
||||
return fmt.Sprintf("file/meta/%s/%s?language=%s", src, url.PathEscape(ext), language)
|
||||
}
|
||||
|
||||
func SlaveFileListRoute(srcPath string, recursive bool) string {
|
||||
base := "file/list"
|
||||
query := url.Values{}
|
||||
query.Set("recursive", strconv.FormatBool(recursive))
|
||||
query.Set("path", srcPath)
|
||||
return fmt.Sprintf("%s?%s", base, query.Encode())
|
||||
}
|
||||
|
||||
func SlaveThumbUrl(base *url.URL, srcPath, ext string) *url.URL {
|
||||
srcPath = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(srcPath)))
|
||||
ext = url.PathEscape(ext)
|
||||
route, _ := url.Parse(constants.APIPrefixSlave + fmt.Sprintf("/file/thumb/%s/%s", srcPath, ext))
|
||||
base = base.ResolveReference(route)
|
||||
return base
|
||||
}
|
||||
|
||||
func SlaveGetTaskRoute(id int, deleteOnComplete bool) string {
|
||||
p := constants.APIPrefixSlave + "/task/" + strconv.Itoa(id)
|
||||
if deleteOnComplete {
|
||||
p += "?" + SlaveClearTaskRegistryQuery + "=true"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func SlavePingRoute(base *url.URL) string {
|
||||
route, _ := url.Parse(constants.APIPrefixSlave + "/ping")
|
||||
return base.ResolveReference(route).String()
|
||||
}
|
||||
Reference in New Issue
Block a user