Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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/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() {
|
||||
@@ -33,133 +42,371 @@ type SettingChangeService struct {
|
||||
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_")
|
||||
//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 (service *NoParamService) Summary() serializer.Response {
|
||||
// 获取版本信息
|
||||
versions := map[string]string{
|
||||
"backend": conf.BackendVersion,
|
||||
"db": conf.RequiredDBVersion,
|
||||
"commit": conf.LastCommit,
|
||||
"is_pro": conf.IsPro,
|
||||
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 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}
|
||||
if summary, ok := kv.Get(MetricCacheKey); ok {
|
||||
summaryCasted := summary.(MetricsSummary)
|
||||
res.MetricsSummary = &summaryCasted
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 统计每日概况
|
||||
total := 12
|
||||
files := make([]int, total)
|
||||
users := make([]int, total)
|
||||
shares := make([]int, total)
|
||||
date := make([]string, total)
|
||||
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 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])
|
||||
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
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
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,
|
||||
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)
|
||||
}
|
||||
|
||||
cache.Set("admin_summary", resp, 86400)
|
||||
return serializer.Response{
|
||||
Data: resp,
|
||||
}
|
||||
_ = 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"`
|
||||
}
|
||||
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) serializer.Response {
|
||||
func (s *ThumbGeneratorTestService) Test(c *gin.Context) (string, error) {
|
||||
version, err := thumb.TestGenerator(c, s.Name, s.Executable)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeParamErr, err.Error(), err)
|
||||
return "", serializer.NewError(serializer.CodeParamErr, "Failed to invoke generator: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: version,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user