Cloudreve/service/admin/site.go

413 lines
14 KiB
Go

package admin
import (
"context"
"encoding/gob"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v4/application/constants"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
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"`
}
// 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.ErrDeprecated(serializer.CodeUpdateSetting, "Setting "+setting.Key+" failed to update", err)
// }
//
// cacheClean = append(cacheClean, setting.Key)
//}
//
//if err := tx.Commit().Error; err != nil {
// return serializer.DBErrDeprecated("Failed to update setting", err)
//}
//
//cache.Deletes(cacheClean, "setting_")
return serializer.Response{}
}
const (
SummaryRangeDays = 12
MetricCacheKey = "admin_summary"
metricErrMsg = "Failed to generate metrics summary"
)
type (
SummaryService struct {
Generate bool `form:"generate"`
}
SummaryParamCtx struct{}
)
// Summary 获取站点统计概况
func (s *SummaryService) Summary(c *gin.Context) (*HomepageSummary, error) {
dep := dependency.FromContext(c)
kv := dep.KV()
res := &HomepageSummary{
Version: &Version{
Version: constants.BackendVersion,
Pro: constants.IsProBool,
Commit: constants.LastCommit,
},
SiteURls: lo.Map(dep.SettingProvider().AllSiteURLs(c), func(item *url.URL, index int) string {
return item.String()
}),
}
if summary, ok := kv.Get(MetricCacheKey); ok {
summaryCasted := summary.(MetricsSummary)
res.MetricsSummary = &summaryCasted
return res, nil
}
if !s.Generate {
return res, nil
}
summary := &MetricsSummary{
Files: make([]int, SummaryRangeDays),
Users: make([]int, SummaryRangeDays),
Shares: make([]int, SummaryRangeDays),
Dates: make([]time.Time, SummaryRangeDays),
GeneratedAt: time.Now(),
}
fileClient := dep.FileClient()
userClient := dep.UserClient()
shareClient := dep.ShareClient()
toRound := time.Now()
timeBase := time.Date(toRound.Year(), toRound.Month(), toRound.Day()+1, 0, 0, 0, 0, toRound.Location())
for day := range summary.Files {
start := timeBase.Add(-time.Duration(SummaryRangeDays-day) * time.Hour * 24)
end := timeBase.Add(-time.Duration(SummaryRangeDays-day-1) * time.Hour * 24)
summary.Dates[day] = start
fileTotal, err := fileClient.CountByTimeRange(c, &start, &end)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
userTotal, err := userClient.CountByTimeRange(c, &start, &end)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
shareTotal, err := shareClient.CountByTimeRange(c, &start, &end)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
summary.Files[day] = fileTotal
summary.Users[day] = userTotal
summary.Shares[day] = shareTotal
}
var err error
summary.FileTotal, err = fileClient.CountByTimeRange(c, nil, nil)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
summary.UserTotal, err = userClient.CountByTimeRange(c, nil, nil)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
summary.ShareTotal, err = shareClient.CountByTimeRange(c, nil, nil)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
summary.EntitiesTotal, err = fileClient.CountEntityByTimeRange(c, nil, nil)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
}
_ = kv.Set(MetricCacheKey, *summary, 86400)
res.MetricsSummary = summary
return res, nil
}
// ThumbGeneratorTestService 缩略图生成测试服务
type (
ThumbGeneratorTestService struct {
Name string `json:"name" binding:"required"`
Executable string `json:"executable" binding:"required"`
}
ThumbGeneratorTestParamCtx struct{}
)
// Test 通过获取生成器版本来测试
func (s *ThumbGeneratorTestService) Test(c *gin.Context) (string, error) {
version, err := thumb.TestGenerator(c, s.Name, s.Executable)
if err != nil {
return "", serializer.NewError(serializer.CodeParamErr, "Failed to invoke generator: "+err.Error(), err)
}
return version, nil
}
type (
GetSettingService struct {
Keys []string `json:"keys" binding:"required"`
}
GetSettingParamCtx struct{}
)
func (s *GetSettingService) GetSetting(c *gin.Context) (map[string]string, error) {
dep := dependency.FromContext(c)
res, err := dep.SettingClient().Gets(c, lo.Filter(s.Keys, func(item string, index int) bool {
return item != "secret_key"
}))
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get settings", err)
}
return res, nil
}
type (
SetSettingService struct {
Settings map[string]string `json:"settings" binding:"required"`
}
SetSettingParamCtx struct{}
SettingPreProcessor func(ctx context.Context, settings map[string]string) error
SettingPostProcessor func(ctx context.Context, settings map[string]string) error
)
var (
preprocessors = map[string]SettingPreProcessor{
"siteURL": siteUrlPreProcessor,
"mime_mapping": mimeMappingPreProcessor,
"secret_key": secretKeyPreProcessor,
}
postprocessors = map[string]SettingPostProcessor{
"mime_mapping": mimeMappingPostProcessor,
"media_meta_exif": mediaMetaPostProcessor,
"media_meta_music": mediaMetaPostProcessor,
"media_meta_ffprobe": mediaMetaPostProcessor,
"smtpUser": emailPostProcessor,
"smtpPass": emailPostProcessor,
"smtpHost": emailPostProcessor,
"smtpPort": emailPostProcessor,
"smtpEncryption": emailPostProcessor,
"smtpFrom": emailPostProcessor,
"replyTo": emailPostProcessor,
"fromName": emailPostProcessor,
"fromAdress": emailPostProcessor,
"queue_media_meta_worker_num": mediaMetaQueuePostProcessor,
"queue_media_meta_max_execution": mediaMetaQueuePostProcessor,
"queue_media_meta_backoff_factor": mediaMetaQueuePostProcessor,
"queue_media_meta_backoff_max_duration": mediaMetaQueuePostProcessor,
"queue_media_meta_max_retry": mediaMetaQueuePostProcessor,
"queue_media_meta_retry_delay": mediaMetaQueuePostProcessor,
"queue_thumb_worker_num": thumbQueuePostProcessor,
"queue_thumb_max_execution": thumbQueuePostProcessor,
"queue_thumb_backoff_factor": thumbQueuePostProcessor,
"queue_thumb_backoff_max_duration": thumbQueuePostProcessor,
"queue_thumb_max_retry": thumbQueuePostProcessor,
"queue_thumb_retry_delay": thumbQueuePostProcessor,
"queue_recycle_worker_num": entityRecycleQueuePostProcessor,
"queue_recycle_max_execution": entityRecycleQueuePostProcessor,
"queue_recycle_backoff_factor": entityRecycleQueuePostProcessor,
"queue_recycle_backoff_max_duration": entityRecycleQueuePostProcessor,
"queue_recycle_max_retry": entityRecycleQueuePostProcessor,
"queue_recycle_retry_delay": entityRecycleQueuePostProcessor,
"queue_io_intense_worker_num": ioIntenseQueuePostProcessor,
"queue_io_intense_max_execution": ioIntenseQueuePostProcessor,
"queue_io_intense_backoff_factor": ioIntenseQueuePostProcessor,
"queue_io_intense_backoff_max_duration": ioIntenseQueuePostProcessor,
"queue_io_intense_max_retry": ioIntenseQueuePostProcessor,
"queue_io_intense_retry_delay": ioIntenseQueuePostProcessor,
"queue_remote_download_worker_num": remoteDownloadQueuePostProcessor,
"queue_remote_download_max_execution": remoteDownloadQueuePostProcessor,
"queue_remote_download_backoff_factor": remoteDownloadQueuePostProcessor,
"queue_remote_download_backoff_max_duration": remoteDownloadQueuePostProcessor,
"queue_remote_download_max_retry": remoteDownloadQueuePostProcessor,
"queue_remote_download_retry_delay": remoteDownloadQueuePostProcessor,
"secret_key": secretKeyPostProcessor,
}
)
func (s *SetSettingService) SetSetting(c *gin.Context) (map[string]string, error) {
dep := dependency.FromContext(c)
kv := dep.KV()
settingClient := dep.SettingClient()
// Preprocess settings
allPreprocessors := make(map[string]SettingPreProcessor)
allPostprocessors := make(map[string]SettingPostProcessor)
for k, _ := range s.Settings {
if preprocessor, ok := preprocessors[k]; ok {
fnName := reflect.TypeOf(preprocessor).Name()
if _, ok := allPreprocessors[fnName]; !ok {
allPreprocessors[fnName] = preprocessor
}
}
if postprocessor, ok := postprocessors[k]; ok {
fnName := reflect.TypeOf(postprocessor).Name()
if _, ok := allPostprocessors[fnName]; !ok {
allPostprocessors[fnName] = postprocessor
}
}
}
// Execute all preprocessors
for _, preprocessor := range allPreprocessors {
if err := preprocessor(c, s.Settings); err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to validate settings", err)
}
}
// Save to db
sc, tx, ctx, err := inventory.WithTx(c, settingClient)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create transaction", err)
}
if err := sc.Set(ctx, s.Settings); err != nil {
_ = inventory.Rollback(tx)
return nil, serializer.NewError(serializer.CodeDBError, "Failed to save settings", err)
}
if err := inventory.Commit(tx); err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err)
}
// Clean cache
if err := kv.Delete(setting.KvSettingPrefix, lo.Keys(s.Settings)...); err != nil {
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to clear cache", err)
}
// Execute post preprocessors
for _, postprocessor := range allPostprocessors {
if err := postprocessor(ctx, s.Settings); err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to post process settings", err)
}
}
return s.Settings, nil
}
func siteUrlPreProcessor(ctx context.Context, settings map[string]string) error {
siteURL := settings["siteURL"]
urls := strings.Split(siteURL, ",")
for index, u := range urls {
urlParsed, err := url.Parse(u)
if err != nil {
return fmt.Errorf("Failed to parse siteURL %q: %w", u, err)
}
urls[index] = urlParsed.String()
}
settings["siteURL"] = strings.Join(urls, ",")
return nil
}
func secretKeyPreProcessor(ctx context.Context, settings map[string]string) error {
settings["secret_key"] = util.RandStringRunes(256)
return nil
}
func mimeMappingPreProcessor(ctx context.Context, settings map[string]string) error {
var mapping map[string]string
if err := json.Unmarshal([]byte(settings["mime_mapping"]), &mapping); err != nil {
return serializer.NewError(serializer.CodeParamErr, "Invalid mime mapping", err)
}
return nil
}
func mimeMappingPostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.MimeDetector(context.WithValue(ctx, dependency.ReloadCtx{}, true))
return nil
}
func mediaMetaPostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.MediaMetaExtractor(context.WithValue(ctx, dependency.ReloadCtx{}, true))
return nil
}
func emailPostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.EmailClient(context.WithValue(ctx, dependency.ReloadCtx{}, true))
return nil
}
func mediaMetaQueuePostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.MediaMetaQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
return nil
}
func ioIntenseQueuePostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.IoIntenseQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
return nil
}
func remoteDownloadQueuePostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.RemoteDownloadQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
return nil
}
func entityRecycleQueuePostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.EntityRecycleQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
return nil
}
func thumbQueuePostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.ThumbQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
return nil
}
func secretKeyPostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.KV().Delete(manager.EntityUrlCacheKeyPrefix)
settings["secret_key"] = ""
return nil
}