This commit is contained in:
2024-02-25 08:30:34 +08:00
commit 4947f39e74
273 changed files with 45396 additions and 0 deletions

71
service/admin/aria2.go Normal file
View File

@ -0,0 +1,71 @@
package admin
import (
"bytes"
"encoding/json"
"net/url"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/aria2"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
)
// Aria2TestService aria2连接测试服务
type Aria2TestService struct {
Server string `json:"server"`
RPC string `json:"rpc" binding:"required"`
Secret string `json:"secret"`
Token string `json:"token"`
Type model.ModelType `json:"type"`
}
// Test 测试aria2连接
func (service *Aria2TestService) TestMaster() serializer.Response {
res, err := aria2.TestRPCConnection(service.RPC, service.Token, 5)
if err != nil {
return serializer.ParamErr("Failed to connect to RPC server: "+err.Error(), err)
}
if res.Version == "" {
return serializer.ParamErr("RPC server returns unexpected response", nil)
}
return serializer.Response{Data: res.Version}
}
func (service *Aria2TestService) TestSlave() serializer.Response {
slave, err := url.Parse(service.Server)
if err != nil {
return serializer.ParamErr("Cannot parse slave server URL, "+err.Error(), nil)
}
controller, _ := url.Parse("/api/v3/slave/ping/aria2")
// 请求正文
service.Type = model.MasterNodeType
bodyByte, _ := json.Marshal(service)
r := request.NewClient()
res, err := r.Request(
"POST",
slave.ResolveReference(controller).String(),
bytes.NewReader(bodyByte),
request.WithTimeout(time.Duration(10)*time.Second),
request.WithCredential(
auth.HMACAuth{SecretKey: []byte(service.Secret)},
int64(model.GetIntSetting("slave_api_timeout", 60)),
),
).DecodeResponse()
if err != nil {
return serializer.ParamErr("Failed to connect to slave node, "+err.Error(), nil)
}
if res.Code != 0 {
return serializer.ParamErr("Successfully connected to slave, but slave returns: "+res.Msg, nil)
}
return serializer.Response{Data: res.Data.(string)}
}

208
service/admin/file.go Normal file
View File

@ -0,0 +1,208 @@
package admin
import (
"context"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/service/explorer"
"github.com/gin-gonic/gin"
)
// FileService 文件ID服务
type FileService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// FileBatchService 文件批量操作服务
type FileBatchService struct {
ID []uint `json:"id" binding:"min=1"`
Force bool `json:"force"`
UnlinkOnly bool `json:"unlink"`
}
// ListFolderService 列目录结构
type ListFolderService struct {
Path string `uri:"path" binding:"required,max=65535"`
ID uint `uri:"id" binding:"required"`
Type string `uri:"type" binding:"eq=policy|eq=user"`
}
// List 列出指定路径下的目录
func (service *ListFolderService) List(c *gin.Context) serializer.Response {
if service.Type == "policy" {
// 列取存储策略中的目录
policy, err := model.GetPolicyByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
// 创建文件系统
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 列取存储策略中的文件
fs.Policy = &policy
res, err := fs.ListPhysical(c.Request.Context(), service.Path)
if err != nil {
return serializer.Err(serializer.CodeListFilesError, "", err)
}
return serializer.Response{
Data: serializer.BuildObjectList(0, res, nil),
}
}
// 列取用户空间目录
// 查找用户
user, err := model.GetUserByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodeUserNotFound, "", err)
}
// 创建文件系统
fs, err := filesystem.NewFileSystem(&user)
if err != nil {
return serializer.Err(serializer.CodeCreateFSError, "", err)
}
defer fs.Recycle()
// 列取目录
res, err := fs.List(c.Request.Context(), service.Path, nil)
if err != nil {
return serializer.Err(serializer.CodeListFilesError, "", err)
}
return serializer.Response{
Data: serializer.BuildObjectList(0, res, nil),
}
}
// Delete 删除文件
func (service *FileBatchService) Delete(c *gin.Context) serializer.Response {
files, err := model.GetFilesByIDs(service.ID, 0)
if err != nil {
return serializer.DBErr("Failed to list files for deleting", err)
}
// 根据用户分组
userFile := make(map[uint][]model.File)
for i := 0; i < len(files); i++ {
if _, ok := userFile[files[i].UserID]; !ok {
userFile[files[i].UserID] = []model.File{}
}
userFile[files[i].UserID] = append(userFile[files[i].UserID], files[i])
}
// 异步执行删除
go func(files map[uint][]model.File) {
for uid, file := range files {
var (
fs *filesystem.FileSystem
err error
)
user, err := model.GetUserByID(uid)
if err != nil {
fs, err = filesystem.NewAnonymousFileSystem()
if err != nil {
continue
}
} else {
fs, err = filesystem.NewFileSystem(&user)
if err != nil {
fs.Recycle()
continue
}
}
// 汇总文件ID
ids := make([]uint, 0, len(file))
for i := 0; i < len(file); i++ {
ids = append(ids, file[i].ID)
}
// 执行删除
fs.Delete(context.Background(), []uint{}, ids, service.Force, service.UnlinkOnly)
fs.Recycle()
}
}(userFile)
// 分组执行删除
return serializer.Response{}
}
// Get 预览文件
func (service *FileService) Get(c *gin.Context) serializer.Response {
file, err := model.GetFilesByIDs([]uint{service.ID}, 0)
if err != nil {
return serializer.Err(serializer.CodeFileNotFound, "", err)
}
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, &file[0])
var subService explorer.FileIDService
res := subService.PreviewContent(ctx, c, false)
return res
}
// Files 列出文件
func (service *AdminListService) Files() serializer.Response {
var res []model.File
total := 0
tx := model.DB.Model(&model.File{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += k + " like '%" + v + "%' OR "
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 查询对应用户
users := make(map[uint]model.User)
for _, file := range res {
users[file.UserID] = model.User{}
}
userIDs := make([]uint, 0, len(users))
for k := range users {
userIDs = append(userIDs, k)
}
var userList []model.User
model.DB.Where("id in (?)", userIDs).Find(&userList)
for _, v := range userList {
users[v.ID] = v
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"users": users,
}}
}

117
service/admin/group.go Normal file
View File

@ -0,0 +1,117 @@
package admin
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"strconv"
)
// AddGroupService 用户组添加服务
type AddGroupService struct {
Group model.Group `json:"group" binding:"required"`
}
// GroupService 用户组ID服务
type GroupService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// Get 获取用户组详情
func (service *GroupService) Get() serializer.Response {
group, err := model.GetGroupByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodeGroupNotFound, "", err)
}
return serializer.Response{Data: group}
}
// Delete 删除用户组
func (service *GroupService) Delete() serializer.Response {
// 查找用户组
group, err := model.GetGroupByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodeGroupNotFound, "", err)
}
// 是否为系统用户组
if group.ID <= 3 {
return serializer.Err(serializer.CodeInvalidActionOnSystemGroup, "", err)
}
// 检查是否有用户使用
total := 0
row := model.DB.Model(&model.User{}).Where("group_id = ?", service.ID).
Select("count(id)").Row()
row.Scan(&total)
if total > 0 {
return serializer.Err(serializer.CodeGroupUsedByUser, strconv.Itoa(total), nil)
}
model.DB.Delete(&group)
return serializer.Response{}
}
// Add 添加用户组
func (service *AddGroupService) Add() serializer.Response {
if service.Group.ID > 0 {
if err := model.DB.Save(&service.Group).Error; err != nil {
return serializer.DBErr("Failed to save group record", err)
}
} else {
if err := model.DB.Create(&service.Group).Error; err != nil {
return serializer.DBErr("Failed to create group record", err)
}
}
return serializer.Response{Data: service.Group.ID}
}
// Groups 列出用户组
func (service *AdminListService) Groups() serializer.Response {
var res []model.Group
total := 0
tx := model.DB.Model(&model.Group{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 统计每个用户组的用户总数
statics := make(map[uint]int, len(res))
for i := 0; i < len(res); i++ {
total := 0
row := model.DB.Model(&model.User{}).Where("group_id = ?", res[i].ID).
Select("count(id)").Row()
row.Scan(&total)
statics[res[i].ID] = total
}
// 汇总用户组存储策略
policies := make(map[uint]model.Policy)
for i := 0; i < len(res); i++ {
for _, p := range res[i].PolicyList {
if _, ok := policies[p]; !ok {
policies[p], _ = model.GetPolicyByID(p)
}
}
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"statics": statics,
"policies": policies,
}}
}

22
service/admin/list.go Normal file
View File

@ -0,0 +1,22 @@
package admin
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
)
// AdminListService 仪表盘列条目服务
type AdminListService struct {
Page int `json:"page" binding:"min=1,required"`
PageSize int `json:"page_size" binding:"min=1,required"`
OrderBy string `json:"order_by"`
Conditions map[string]string `form:"conditions"`
Searches map[string]string `form:"searches"`
}
// GroupList 获取用户组列表
func (service *NoParamService) GroupList() serializer.Response {
var res []model.Group
model.DB.Model(&model.Group{}).Find(&res)
return serializer.Response{Data: res}
}

142
service/admin/node.go Normal file
View File

@ -0,0 +1,142 @@
package admin
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"strings"
)
// AddNodeService 节点添加服务
type AddNodeService struct {
Node model.Node `json:"node" binding:"required"`
}
// Add 添加节点
func (service *AddNodeService) Add() serializer.Response {
if service.Node.ID > 0 {
if err := model.DB.Save(&service.Node).Error; err != nil {
return serializer.DBErr("Failed to save node record", err)
}
} else {
if err := model.DB.Create(&service.Node).Error; err != nil {
return serializer.DBErr("Failed to create node record", err)
}
}
if service.Node.Status == model.NodeActive {
cluster.Default.Add(&service.Node)
}
return serializer.Response{Data: service.Node.ID}
}
// Nodes 列出从机节点
func (service *AdminListService) Nodes() serializer.Response {
var res []model.Node
total := 0
tx := model.DB.Model(&model.Node{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += k + " like '%" + v + "%' OR "
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
isActive := make(map[uint]bool)
for i := 0; i < len(res); i++ {
if node := cluster.Default.GetNodeByID(res[i].ID); node != nil {
isActive[res[i].ID] = node.IsActive()
}
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"active": isActive,
}}
}
// ToggleNodeService 开关节点服务
type ToggleNodeService struct {
ID uint `uri:"id"`
Desired model.NodeStatus `uri:"desired"`
}
// Toggle 开关节点
func (service *ToggleNodeService) Toggle() serializer.Response {
node, err := model.GetNodeByID(service.ID)
if err != nil {
return serializer.DBErr("Node not found", err)
}
// 是否为系统节点
if node.ID <= 1 {
return serializer.Err(serializer.CodeInvalidActionOnSystemNode, "", err)
}
if err = node.SetStatus(service.Desired); err != nil {
return serializer.DBErr("Failed to change node status", err)
}
if service.Desired == model.NodeActive {
cluster.Default.Add(&node)
} else {
cluster.Default.Delete(node.ID)
}
return serializer.Response{}
}
// NodeService 节点ID服务
type NodeService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// Delete 删除节点
func (service *NodeService) Delete() serializer.Response {
// 查找用户组
node, err := model.GetNodeByID(service.ID)
if err != nil {
return serializer.DBErr("Node record not found", err)
}
// 是否为系统节点
if node.ID <= 1 {
return serializer.Err(serializer.CodeInvalidActionOnSystemNode, "", err)
}
cluster.Default.Delete(node.ID)
if err := model.DB.Delete(&node).Error; err != nil {
return serializer.DBErr("Failed to delete node record", err)
}
return serializer.Response{}
}
// Get 获取节点详情
func (service *NodeService) Get() serializer.Response {
node, err := model.GetNodeByID(service.ID)
if err != nil {
return serializer.DBErr("Node not exist", err)
}
return serializer.Response{Data: node}
}

75
service/admin/order.go Normal file
View File

@ -0,0 +1,75 @@
package admin
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
"strings"
)
// OrderBatchService 订单批量操作服务
type OrderBatchService struct {
ID []uint `json:"id" binding:"min=1"`
}
// Delete 删除订单
func (service *OrderBatchService) Delete(c *gin.Context) serializer.Response {
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Order{}).Error; err != nil {
return serializer.DBErr("Failed to delete order records.", err)
}
return serializer.Response{}
}
// Orders 列出订单
func (service *AdminListService) Orders() serializer.Response {
var res []model.Order
total := 0
tx := model.DB.Model(&model.Order{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += k + " like '%" + v + "%' OR "
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 查询对应用户同时计算HashID
users := make(map[uint]model.User)
for _, file := range res {
users[file.UserID] = model.User{}
}
userIDs := make([]uint, 0, len(users))
for k := range users {
userIDs = append(userIDs, k)
}
var userList []model.User
model.DB.Where("id in (?)", userIDs).Find(&userList)
for _, v := range userList {
users[v.ID] = v
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"users": users,
}}
}

360
service/admin/policy.go Normal file
View File

@ -0,0 +1,360 @@
package admin
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/googledrive"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/cos"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/oss"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/s3"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
cossdk "github.com/tencentyun/cos-go-sdk-v5"
)
// PathTestService 本地路径测试服务
type PathTestService struct {
Path string `json:"path" binding:"required"`
}
// SlaveTestService 从机测试服务
type SlaveTestService struct {
Secret string `json:"secret" binding:"required"`
Server string `json:"server" binding:"required"`
}
// SlavePingService 从机相应ping
type SlavePingService struct {
Callback string `json:"callback" binding:"required"`
}
// AddPolicyService 存储策略添加服务
type AddPolicyService struct {
Policy model.Policy `json:"policy" binding:"required"`
}
// PolicyService 存储策略ID服务
type PolicyService struct {
ID uint `uri:"id" json:"id" binding:"required"`
Region string `json:"region"`
}
// Delete 删除存储策略
func (service *PolicyService) Delete() serializer.Response {
// 禁止删除默认策略
if service.ID == 1 {
return serializer.Err(serializer.CodeDeleteDefaultPolicy, "", nil)
}
policy, err := model.GetPolicyByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
// 检查是否有文件使用
total := 0
row := model.DB.Model(&model.File{}).Where("policy_id = ?", service.ID).
Select("count(id)").Row()
row.Scan(&total)
if total > 0 {
return serializer.Err(serializer.CodePolicyUsedByFiles, strconv.Itoa(total), nil)
}
// 检查用户组使用
var groups []model.Group
model.DB.Model(&model.Group{}).Where(
"policies like ? OR policies like ? OR policies like ? OR policies like ?",
fmt.Sprintf("[%d,%%", service.ID),
fmt.Sprintf("%%,%d]", service.ID),
fmt.Sprintf("%%,%d,%%", service.ID),
fmt.Sprintf("%%[%d]%%", service.ID),
).Find(&groups)
if len(groups) > 0 {
return serializer.Err(serializer.CodePolicyUsedByGroups, strconv.Itoa(len(groups)), nil)
}
model.DB.Delete(&policy)
policy.ClearCache()
return serializer.Response{}
}
// Get 获取存储策略详情
func (service *PolicyService) Get() serializer.Response {
policy, err := model.GetPolicyByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", err)
}
return serializer.Response{Data: policy}
}
// GetOAuth 获取 OneDrive OAuth 地址
func (service *PolicyService) GetOAuth(c *gin.Context, policyType string) serializer.Response {
policy, err := model.GetPolicyByID(service.ID)
if err != nil || policy.Type != policyType {
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
}
util.SetSession(c, map[string]interface{}{
policyType + "_oauth_policy": policy.ID,
})
var redirect string
switch policy.Type {
case "onedrive":
client, err := onedrive.NewClient(&policy)
if err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to initialize OneDrive client", err)
}
redirect = client.OAuthURL(context.Background(), []string{
"offline_access",
"files.readwrite.all",
})
case "googledrive":
client, err := googledrive.NewClient(&policy)
if err != nil {
return serializer.Err(serializer.CodeInternalSetting, "Failed to initialize Google Drive client", err)
}
redirect = client.OAuthURL(context.Background(), googledrive.RequiredScope)
}
// Delete token cache
cache.Deletes([]string{policy.BucketName}, policyType+"_")
return serializer.Response{Data: redirect}
}
// AddSCF 创建回调云函数
func (service *PolicyService) AddSCF() serializer.Response {
policy, err := model.GetPolicyByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
}
if err := cos.CreateSCF(&policy, service.Region); err != nil {
return serializer.ParamErr("Failed to create SCF function", err)
}
return serializer.Response{}
}
// AddCORS 创建跨域策略
func (service *PolicyService) AddCORS() serializer.Response {
policy, err := model.GetPolicyByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
}
switch policy.Type {
case "oss":
handler, err := oss.NewDriver(&policy)
if err != nil {
return serializer.Err(serializer.CodeAddCORS, "", err)
}
if err := handler.CORS(); err != nil {
return serializer.Err(serializer.CodeAddCORS, "", err)
}
case "cos":
u, _ := url.Parse(policy.Server)
b := &cossdk.BaseURL{BucketURL: u}
handler := cos.Driver{
Policy: &policy,
HTTPClient: request.NewClient(),
Client: cossdk.NewClient(b, &http.Client{
Transport: &cossdk.AuthorizationTransport{
SecretID: policy.AccessKey,
SecretKey: policy.SecretKey,
},
}),
}
if err := handler.CORS(); err != nil {
return serializer.Err(serializer.CodeAddCORS, "", err)
}
case "s3":
handler, err := s3.NewDriver(&policy)
if err != nil {
return serializer.Err(serializer.CodeAddCORS, "", err)
}
if err := handler.CORS(); err != nil {
return serializer.Err(serializer.CodeAddCORS, "", err)
}
default:
return serializer.Err(serializer.CodePolicyNotAllowed, "", nil)
}
return serializer.Response{}
}
// Test 从机响应ping
func (service *SlavePingService) Test() serializer.Response {
master, err := url.Parse(service.Callback)
if err != nil {
return serializer.ParamErr("Failed to parse Master site url: "+err.Error(), nil)
}
controller, _ := url.Parse("/api/v3/site/ping")
r := request.NewClient()
res, err := r.Request(
"GET",
master.ResolveReference(controller).String(),
nil,
request.WithTimeout(time.Duration(10)*time.Second),
).DecodeResponse()
if err != nil {
return serializer.Err(serializer.CodeSlavePingMaster, err.Error(), nil)
}
version := conf.BackendVersion
if conf.IsPlus == "true" {
version += "-plus"
}
if res.Data.(string) != version {
return serializer.Err(serializer.CodeVersionMismatch, "Master: "+res.Data.(string)+", Slave: "+version, nil)
}
return serializer.Response{}
}
// Test 测试从机通信
func (service *SlaveTestService) Test() serializer.Response {
slave, err := url.Parse(service.Server)
if err != nil {
return serializer.ParamErr("Failed to parse slave node server URL: "+err.Error(), nil)
}
controller, _ := url.Parse("/api/v3/slave/ping")
// 请求正文
body := map[string]string{
"callback": model.GetSiteURL().String(),
}
bodyByte, _ := json.Marshal(body)
r := request.NewClient()
res, err := r.Request(
"POST",
slave.ResolveReference(controller).String(),
bytes.NewReader(bodyByte),
request.WithTimeout(time.Duration(10)*time.Second),
request.WithCredential(
auth.HMACAuth{SecretKey: []byte(service.Secret)},
int64(model.GetIntSetting("slave_api_timeout", 60)),
),
).DecodeResponse()
if err != nil {
return serializer.ParamErr("Failed to connect to slave node: "+err.Error(), nil)
}
if res.Code != 0 {
return serializer.ParamErr("Successfully connected to slave node, but slave returns: "+res.Msg, nil)
}
return serializer.Response{}
}
// Add 添加存储策略
func (service *AddPolicyService) Add() serializer.Response {
if service.Policy.Type != "local" && service.Policy.Type != "remote" {
service.Policy.DirNameRule = strings.TrimPrefix(service.Policy.DirNameRule, "/")
}
if service.Policy.ID > 0 {
if err := model.DB.Save(&service.Policy).Error; err != nil {
return serializer.DBErr("Failed to save policy", err)
}
} else {
if err := model.DB.Create(&service.Policy).Error; err != nil {
return serializer.DBErr("Failed to create policy", err)
}
}
service.Policy.ClearCache()
return serializer.Response{Data: service.Policy.ID}
}
// Test 测试本地路径
func (service *PathTestService) Test() serializer.Response {
policy := model.Policy{DirNameRule: service.Path}
path := policy.GeneratePath(1, "/My File")
path = filepath.Join(path, "test.txt")
file, err := util.CreatNestedFile(util.RelativePath(path))
if err != nil {
return serializer.ParamErr(fmt.Sprintf("Failed to create \"%s\": %s", path, err.Error()), nil)
}
file.Close()
os.Remove(path)
return serializer.Response{}
}
// Policies 列出存储策略
func (service *AdminListService) Policies() serializer.Response {
var res []model.Policy
total := 0
tx := model.DB.Model(&model.Policy{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 统计每个策略的文件使用
statics := make(map[uint][2]int, len(res))
policyIds := make([]uint, 0, len(res))
for i := 0; i < len(res); i++ {
policyIds = append(policyIds, res[i].ID)
}
rows, _ := model.DB.Model(&model.File{}).Where("policy_id in (?)", policyIds).
Select("policy_id,count(id),sum(size)").Group("policy_id").Rows()
for rows.Next() {
policyId := uint(0)
total := [2]int{}
rows.Scan(&policyId, &total[0], &total[1])
statics[policyId] = total
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"statics": statics,
}}
}

72
service/admin/report.go Normal file
View File

@ -0,0 +1,72 @@
package admin
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
)
// ReportBatchService 任务批量操作服务
type ReportBatchService struct {
ID []uint `json:"id" binding:"min=1"`
}
// Reports 批量删除举报
func (service *ReportBatchService) Delete() serializer.Response {
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Report{}).Error; err != nil {
return serializer.DBErr("Failed to change report status", err)
}
return serializer.Response{}
}
// Reports 列出待处理举报
func (service *AdminListService) Reports() serializer.Response {
var res []model.Report
total := 0
tx := model.DB.Model(&model.Report{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Set("gorm:auto_preload", true).Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 计算分享的 HashID
hashIDs := make(map[uint]string, len(res))
for _, report := range res {
hashIDs[report.Share.ID] = hashid.HashID(report.Share.ID, hashid.ShareID)
}
// 查询对应用户
users := make(map[uint]model.User)
for _, report := range res {
users[report.Share.UserID] = model.User{}
}
userIDs := make([]uint, 0, len(users))
for k := range users {
userIDs = append(userIDs, k)
}
var userList []model.User
model.DB.Where("id in (?)", userIDs).Find(&userList)
for _, v := range userList {
users[v.ID] = v
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"users": users,
"ids": hashIDs,
}}
}

80
service/admin/share.go Normal file
View File

@ -0,0 +1,80 @@
package admin
import (
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
)
// ShareBatchService 分享批量操作服务
type ShareBatchService struct {
ID []uint `json:"id" binding:"min=1"`
}
// Delete 删除文件
func (service *ShareBatchService) Delete(c *gin.Context) serializer.Response {
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Share{}).Error; err != nil {
return serializer.DBErr("Failed to delete share record", err)
}
return serializer.Response{}
}
// Shares 列出分享
func (service *AdminListService) Shares() serializer.Response {
var res []model.Share
total := 0
tx := model.DB.Model(&model.Share{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += k + " like '%" + v + "%' OR "
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 查询对应用户同时计算HashID
users := make(map[uint]model.User)
hashIDs := make(map[uint]string, len(res))
for _, file := range res {
users[file.UserID] = model.User{}
hashIDs[file.ID] = hashid.HashID(file.ID, hashid.ShareID)
}
userIDs := make([]uint, 0, len(users))
for k := range users {
userIDs = append(userIDs, k)
}
var userList []model.User
model.DB.Where("id in (?)", userIDs).Find(&userList)
for _, v := range userList {
users[v.ID] = v
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"users": users,
"ids": hashIDs,
}}
}

199
service/admin/site.go Normal file
View File

@ -0,0 +1,199 @@
package admin
import (
"encoding/gob"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/email"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
"github.com/cloudreve/Cloudreve/v3/pkg/vol"
"github.com/gin-gonic/gin"
)
func init() {
gob.Register(map[string]interface{}{})
gob.Register(map[string]string{})
}
// NoParamService 无需参数的服务
type NoParamService struct {
}
// BatchSettingChangeService 设定批量更改服务
type BatchSettingChangeService struct {
Options []SettingChangeService `json:"options"`
}
// SettingChangeService 设定更改服务
type SettingChangeService struct {
Key string `json:"key" binding:"required"`
Value string `json:"value"`
}
// BatchSettingGet 设定批量获取服务
type BatchSettingGet struct {
Keys []string `json:"keys"`
}
// MailTestService 邮件测试服务
type MailTestService struct {
Email string `json:"to" binding:"email"`
}
// Send 发送测试邮件
func (service *MailTestService) Send() serializer.Response {
if err := email.Send(service.Email, "Cloudreve Email delivery test", "This is a test Email, to test Cloudreve Email delivery settings"); err != nil {
return serializer.Err(serializer.CodeFailedSendEmail, err.Error(), nil)
}
return serializer.Response{}
}
// Get 获取设定值
func (service *BatchSettingGet) Get() serializer.Response {
options := model.GetSettingByNames(service.Keys...)
return serializer.Response{Data: options}
}
// Change 批量更改站点设定
func (service *BatchSettingChangeService) Change() serializer.Response {
cacheClean := make([]string, 0, len(service.Options))
tx := model.DB.Begin()
for _, setting := range service.Options {
if err := tx.Model(&model.Setting{}).Where("name = ?", setting.Key).Update("value", setting.Value).Error; err != nil {
cache.Deletes(cacheClean, "setting_")
tx.Rollback()
return serializer.Err(serializer.CodeUpdateSetting, "Setting "+setting.Key+" failed to update", err)
}
cacheClean = append(cacheClean, setting.Key)
}
if err := tx.Commit().Error; err != nil {
return serializer.DBErr("Failed to update setting", err)
}
cache.Deletes(cacheClean, "setting_")
return serializer.Response{}
}
// Summary 获取站点统计概况
func (service *NoParamService) Summary() serializer.Response {
// 获取版本信息
versions := map[string]string{
"backend": conf.BackendVersion,
"db": conf.RequiredDBVersion,
"commit": conf.LastCommit,
"is_plus": conf.IsPlus,
}
if res, ok := cache.Get("admin_summary"); ok {
resMap := res.(map[string]interface{})
resMap["version"] = versions
resMap["siteURL"] = model.GetSettingByName("siteURL")
return serializer.Response{Data: resMap}
}
// 统计每日概况
total := 12
files := make([]int, total)
users := make([]int, total)
shares := make([]int, total)
date := make([]string, total)
toRound := time.Now()
timeBase := time.Date(toRound.Year(), toRound.Month(), toRound.Day()+1, 0, 0, 0, 0, toRound.Location())
for day := range files {
start := timeBase.Add(-time.Duration(total-day) * time.Hour * 24)
end := timeBase.Add(-time.Duration(total-day-1) * time.Hour * 24)
date[day] = start.Format("1月2日")
model.DB.Model(&model.User{}).Where("created_at BETWEEN ? AND ?", start, end).Count(&users[day])
model.DB.Model(&model.File{}).Where("created_at BETWEEN ? AND ?", start, end).Count(&files[day])
model.DB.Model(&model.Share{}).Where("created_at BETWEEN ? AND ?", start, end).Count(&shares[day])
}
// 统计总数
fileTotal := 0
userTotal := 0
publicShareTotal := 0
secretShareTotal := 0
model.DB.Model(&model.User{}).Count(&userTotal)
model.DB.Model(&model.File{}).Count(&fileTotal)
model.DB.Model(&model.Share{}).Where("password = ?", "").Count(&publicShareTotal)
model.DB.Model(&model.Share{}).Where("password <> ?", "").Count(&secretShareTotal)
resp := map[string]interface{}{
"date": date,
"files": files,
"users": users,
"shares": shares,
"version": versions,
"siteURL": model.GetSettingByName("siteURL"),
"fileTotal": fileTotal,
"userTotal": userTotal,
"publicShareTotal": publicShareTotal,
"secretShareTotal": secretShareTotal,
}
cache.Set("admin_summary", resp, 86400)
return serializer.Response{
Data: resp,
}
}
// ThumbGeneratorTestService 缩略图生成测试服务
type ThumbGeneratorTestService struct {
Name string `json:"name" binding:"required"`
Executable string `json:"executable" binding:"required"`
}
// Test 通过获取生成器版本来测试
func (s *ThumbGeneratorTestService) Test(c *gin.Context) serializer.Response {
version, err := thumb.TestGenerator(c, s.Name, s.Executable)
if err != nil {
return serializer.Err(serializer.CodeParamErr, err.Error(), err)
}
return serializer.Response{
Data: version,
}
}
// VOL 授权管理服务
type VolService struct {
}
// Sync 同步 VOL 授权
func (s *VolService) Sync() serializer.Response {
volClient := vol.New(vol.ClientSecret)
content, signature, err := volClient.Sync()
if err != nil {
return serializer.Err(serializer.CodeInternalSetting, err.Error(), err)
}
subService := &BatchSettingChangeService{
Options: []SettingChangeService{
{
Key: "vol_content",
Value: content,
},
{
Key: "vol_signature",
Value: signature,
},
},
}
res := subService.Change()
if res.Code != 0 {
return res
}
return serializer.Response{Data: content}
}

159
service/admin/task.go Normal file
View File

@ -0,0 +1,159 @@
package admin
import (
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/task"
"github.com/gin-gonic/gin"
)
// TaskBatchService 任务批量操作服务
type TaskBatchService struct {
ID []uint `json:"id" binding:"min=1"`
}
// ImportTaskService 导入任务
type ImportTaskService struct {
UID uint `json:"uid" binding:"required"`
PolicyID uint `json:"policy_id" binding:"required"`
Src string `json:"src" binding:"required,min=1,max=65535"`
Dst string `json:"dst" binding:"required,min=1,max=65535"`
Recursive bool `json:"recursive"`
}
// Create 新建导入任务
func (service *ImportTaskService) Create(c *gin.Context, user *model.User) serializer.Response {
// 创建任务
job, err := task.NewImportTask(service.UID, service.PolicyID, service.Src, service.Dst, service.Recursive)
if err != nil {
return serializer.DBErr("Failed to create task record.", err)
}
task.TaskPoll.Submit(job)
return serializer.Response{}
}
// Delete 删除任务
func (service *TaskBatchService) Delete(c *gin.Context) serializer.Response {
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Download{}).Error; err != nil {
return serializer.DBErr("Failed to delete task records", err)
}
return serializer.Response{}
}
// DeleteGeneral 删除常规任务
func (service *TaskBatchService) DeleteGeneral(c *gin.Context) serializer.Response {
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Task{}).Error; err != nil {
return serializer.DBErr("Failed to delete task records", err)
}
return serializer.Response{}
}
// Tasks 列出常规任务
func (service *AdminListService) Tasks() serializer.Response {
var res []model.Task
total := 0
tx := model.DB.Model(&model.Task{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += k + " like '%" + v + "%' OR "
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 查询对应用户同时计算HashID
users := make(map[uint]model.User)
for _, file := range res {
users[file.UserID] = model.User{}
}
userIDs := make([]uint, 0, len(users))
for k := range users {
userIDs = append(userIDs, k)
}
var userList []model.User
model.DB.Where("id in (?)", userIDs).Find(&userList)
for _, v := range userList {
users[v.ID] = v
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"users": users,
}}
}
// Downloads 列出离线下载任务
func (service *AdminListService) Downloads() serializer.Response {
var res []model.Download
total := 0
tx := model.DB.Model(&model.Download{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += k + " like '%" + v + "%' OR "
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 查询对应用户同时计算HashID
users := make(map[uint]model.User)
for _, file := range res {
users[file.UserID] = model.User{}
}
userIDs := make([]uint, 0, len(users))
for k := range users {
userIDs = append(userIDs, k)
}
var userList []model.User
model.DB.Where("id in (?)", userIDs).Find(&userList)
for _, v := range userList {
users[v.ID] = v
}
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
"users": users,
}}
}

179
service/admin/user.go Normal file
View File

@ -0,0 +1,179 @@
package admin
import (
"context"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
)
// AddUserService 用户添加服务
type AddUserService struct {
User model.User `json:"User" binding:"required"`
Password string `json:"password"`
}
// UserService 用户ID服务
type UserService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// UserBatchService 用户批量操作服务
type UserBatchService struct {
ID []uint `json:"id" binding:"min=1"`
}
// Ban 封禁/解封用户
func (service *UserService) Ban() serializer.Response {
user, err := model.GetUserByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodeUserNotFound, "", err)
}
if user.ID == 1 {
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", err)
}
if user.Status == model.Active {
user.SetStatus(model.Baned)
} else {
user.SetStatus(model.Active)
}
return serializer.Response{Data: user.Status}
}
// Delete 删除用户
func (service *UserBatchService) Delete() serializer.Response {
for _, uid := range service.ID {
user, err := model.GetUserByID(uid)
if err != nil {
return serializer.Err(serializer.CodeUserNotFound, "", err)
}
// 不能删除初始用户
if uid == 1 {
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", err)
}
// 删除与此用户相关的所有资源
fs, err := filesystem.NewFileSystem(&user)
// 删除所有文件
root, err := fs.User.Root()
if err != nil {
return serializer.Err(serializer.CodeInternalSetting, "User's root folder not exist", err)
}
fs.Delete(context.Background(), []uint{root.ID}, []uint{}, false, false)
// 删除相关任务
model.DB.Where("user_id = ?", uid).Delete(&model.Download{})
model.DB.Where("user_id = ?", uid).Delete(&model.Task{})
// 删除订单记录
model.DB.Where("user_id = ?", uid).Delete(&model.Order{})
// 删除容量包
model.DB.Where("user_id = ?", uid).Delete(&model.StoragePack{})
// 删除标签
model.DB.Where("user_id = ?", uid).Delete(&model.Tag{})
// 删除WebDAV账号
model.DB.Where("user_id = ?", uid).Delete(&model.Webdav{})
// 删除此用户
model.DB.Unscoped().Delete(user)
}
return serializer.Response{}
}
// Get 获取用户详情
func (service *UserService) Get() serializer.Response {
group, err := model.GetUserByID(service.ID)
if err != nil {
return serializer.Err(serializer.CodeUserNotFound, "", err)
}
return serializer.Response{Data: group}
}
// Add 添加用户
func (service *AddUserService) Add() serializer.Response {
if service.User.ID > 0 {
user, _ := model.GetUserByID(service.User.ID)
if service.Password != "" {
user.SetPassword(service.Password)
}
// 只更新必要字段
user.Nick = service.User.Nick
user.Email = service.User.Email
user.GroupID = service.User.GroupID
user.Status = service.User.Status
user.Score = service.User.Score
user.TwoFactor = service.User.TwoFactor
// 检查愚蠢操作
if user.ID == 1 {
if user.GroupID != 1 {
return serializer.Err(serializer.CodeChangeGroupForDefaultUser, "", nil)
}
if user.Status != model.Active {
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", nil)
}
}
if err := model.DB.Save(&user).Error; err != nil {
return serializer.DBErr("Failed to save user record", err)
}
} else {
service.User.SetPassword(service.Password)
if err := model.DB.Create(&service.User).Error; err != nil {
return serializer.DBErr("Failed to create user record", err)
}
}
return serializer.Response{Data: service.User.ID}
}
// Users 列出用户
func (service *AdminListService) Users() serializer.Response {
var res []model.User
total := 0
tx := model.DB.Model(&model.User{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where(k+" = ?", v)
}
if len(service.Searches) > 0 {
search := ""
for k, v := range service.Searches {
search += (k + " like '%" + v + "%' OR ")
}
search = strings.TrimSuffix(search, " OR ")
tx = tx.Where(search)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Set("gorm:auto_preload", true).Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
// 补齐缺失用户组
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
}}
}

98
service/admin/vas.go Normal file
View File

@ -0,0 +1,98 @@
package admin
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gofrs/uuid"
)
// GenerateRedeemsService 兑换码生成服务
type GenerateRedeemsService struct {
Num int `json:"num" binding:"required,min=1,max=100"`
ID int64 `json:"id"`
Time int `json:"time" binding:"required,min=1"`
Type int `json:"type" binding:"min=0,max=2"`
}
// SingleIDService 单ID服务
type SingleIDService struct {
ID uint `uri:"id" binding:"required"`
}
// DeleteRedeem 删除兑换码
func (service *SingleIDService) DeleteRedeem() serializer.Response {
if err := model.DB.Where("id = ?", service.ID).Delete(&model.Redeem{}).Error; err != nil {
return serializer.DBErr("Failed to delete gift code record.", err)
}
return serializer.Response{}
}
// Generate 生成兑换码
func (service *GenerateRedeemsService) Generate() serializer.Response {
res := make([]string, service.Num)
redeem := model.Redeem{}
// 开始事务
tx := model.DB.Begin()
if err := tx.Error; err != nil {
return serializer.DBErr("Cannot start transaction", err)
}
// 创建每个兑换码
for i := 0; i < service.Num; i++ {
redeem.Model.ID = 0
redeem.Num = service.Time
redeem.Type = service.Type
redeem.ProductID = service.ID
redeem.Used = false
// 生成唯一兑换码
u2, err := uuid.NewV4()
if err != nil {
tx.Rollback()
return serializer.Err(serializer.CodeInternalSetting, "Failed to generate UUID", err)
}
redeem.Code = u2.String()
if err := tx.Create(&redeem).Error; err != nil {
tx.Rollback()
return serializer.DBErr("Failed to insert gift code record", err)
}
res[i] = redeem.Code
}
if err := tx.Commit().Error; err != nil {
return serializer.DBErr("Failed to insert gift code record", err)
}
return serializer.Response{Data: res}
}
// Redeems 列出激活码
func (service *AdminListService) Redeems() serializer.Response {
var res []model.Redeem
total := 0
tx := model.DB.Model(&model.Redeem{})
if service.OrderBy != "" {
tx = tx.Order(service.OrderBy)
}
for k, v := range service.Conditions {
tx = tx.Where("? = ?", k, v)
}
// 计算总数用于分页
tx.Count(&total)
// 查询记录
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
return serializer.Response{Data: map[string]interface{}{
"total": total,
"items": res,
}}
}