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 }