package api import ( "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "fileshare/internal/response" "fileshare/internal/settings" ) // fileSizeUnits 文件大小单位(对齐参考 FILE_SIZE_UNITS)。 var fileSizeUnits = map[string]int64{"KB": 1024, "MB": 1024 * 1024, "GB": 1024 * 1024 * 1024} // saveTimeUnits 保存时间单位(秒)。 var saveTimeUnits = map[string]int64{"second": 1, "minute": 60, "hour": 3600, "day": 86400} // expireStyleOptions 可用过期方式(用于 setup 表单校验)。 var expireStyleOptions = []string{"day", "hour", "minute", "forever", "count"} // setupFormValue 取表单/JSON 字符串值。 func setupFormValue(data map[string]any, key, def string) string { v, ok := data[key] if !ok || v == nil { return def } if s, ok := v.(string); ok { return s } return strings.TrimSpace(strconv.FormatFloat(toAnyFloat(v), 'f', -1, 64)) } func toAnyFloat(v any) float64 { switch n := v.(type) { case float64: return n case int: return float64(n) case int64: return float64(n) } return 0 } // registerSetup 注册初始化向导(未初始化时唯一可用入口,白名单 /setup)。 func registerSetup(r *gin.Engine, d *Deps) { r.GET("/setup", func(c *gin.Context) { if d.Mgr.IsInitialized() { c.Redirect(http.StatusSeeOther, "/") return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(buildSetupPage(""))) }) r.POST("/setup", func(c *gin.Context) { if d.Mgr.IsInitialized() { c.Redirect(http.StatusSeeOther, "/") return } d.setupSubmit(c) }) } // setupSubmit 处理初始化提交(对齐参考 setup_submit + parse_setup_options)。 func (d *Deps) setupSubmit(c *gin.Context) { // 兼容 JSON 与表单 data := map[string]any{} if strings.Contains(c.GetHeader("Content-Type"), "application/json") { if err := c.ShouldBindJSON(&data); err != nil { c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(buildSetupPage("请求体格式错误"))) return } } else if err := c.Request.ParseForm(); err != nil { c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(buildSetupPage("请求体格式错误"))) return } else { // 多值字段(如多个 expireStyle 复选框)保留完整列表,单值取首项 for k, v := range c.Request.PostForm { switch { case len(v) == 1: data[k] = v[0] case len(v) > 1: data[k] = v } } } adminPassword := setupFormValue(data, "admin_password", "") confirmPassword := setupFormValue(data, "confirm_password", "") siteName := setupFormValue(data, "site_name", "") if adminPassword == "" || len(adminPassword) < 8 { c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(buildSetupPage("管理员密码至少 8 位"))) return } if adminPassword != confirmPassword { c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(buildSetupPage("两次输入的管理员密码不一致"))) return } patch, errMsg := parseSetupOptions(data) if errMsg != "" { c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(buildSetupPage(errMsg))) return } patch["site_name"] = firstNonEmpty(siteName, "文件快传") patch["admin_token"] = settings.HashPassword(adminPassword) patch["jwt_secret"] = settings.GenerateJWTSecret() ctx := c.Request.Context() if err := d.Mgr.UpdateKV(ctx, patch); err != nil { c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte(buildSetupPage("初始化失败: "+err.Error()))) return } if err := d.Mgr.Reload(ctx); err != nil { c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte(buildSetupPage("配置重载失败: "+err.Error()))) return } d.syncRateRules() // JSON 请求返回 JSON;表单返回成功页 if strings.Contains(c.GetHeader("Accept"), "application/json") || strings.Contains(c.GetHeader("Content-Type"), "application/json") { response.OK(c, gin.H{"ok": true, "admin": "/#/admin"}) return } c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(buildSetupSuccessPage())) } // parseSetupOptions 解析并校验初始化选项(对齐参考 parse_setup_options)。 func parseSetupOptions(data map[string]any) (map[string]any, string) { out := map[string]any{} // 文件大小限制 unit := strings.ToUpper(setupFormValue(data, "upload_size_unit", "MB")) if _, ok := fileSizeUnits[unit]; !ok { return nil, "文件大小单位不正确" } sizeVal, err := strconv.Atoi(setupFormValue(data, "upload_size_value", "10")) if err != nil || sizeVal < 1 { return nil, "文件大小限制必须是正整数" } out["uploadSize"] = int64(sizeVal) * fileSizeUnits[unit] // 最长保存时间 saveUnit := strings.ToLower(setupFormValue(data, "save_time_unit", "day")) if _, ok := saveTimeUnits[saveUnit]; !ok { return nil, "最长保存时间单位不正确" } saveVal, err := strconv.Atoi(setupFormValue(data, "save_time_value", "0")) if err != nil || saveVal < 0 { return nil, "最长保存时间必须是非负整数" } out["max_save_seconds"] = int64(saveVal) * saveTimeUnits[saveUnit] // 过期方式白名单 var styles []string if raw, ok := data["expireStyle"]; ok { switch v := raw.(type) { case []any: for _, item := range v { if s, ok := item.(string); ok { styles = append(styles, s) } } case []string: styles = v case string: for _, s := range strings.Split(v, ",") { styles = append(styles, strings.TrimSpace(s)) } } } valid := map[string]bool{} var finalStyles []string for _, s := range styles { s = strings.TrimSpace(s) if s == "" || valid[s] { continue } for _, opt := range expireStyleOptions { if opt == s { valid[s] = true finalStyles = append(finalStyles, s) break } } } if len(finalStyles) == 0 { return nil, "至少需要选择一种过期方式" } out["expireStyle"] = finalStyles // 取件码类型 codeType := setupFormValue(data, "code_generate_type", "secret") if codeType != "number" && codeType != "secret" { return nil, "提取码类型不正确" } out["code_generate_type"] = codeType // 频率限制 for _, item := range []struct{ key, def string }{ {"errorCount", "10"}, {"errorMinute", "1"}, {"loginCount", "5"}, {"loginMinute", "15"}, {"uploadCount", "10"}, {"uploadMinute", "1"}, } { n, err := strconv.Atoi(setupFormValue(data, item.key, item.def)) if err != nil || n < 1 { return nil, item.key + " 必须是正整数" } out[item.key] = n } // 布尔开关 out["openUpload"] = boolToInt(parseSetupBool(data, "openUpload", true)) out["enableChunk"] = boolToInt(parseSetupBool(data, "enableChunk", false)) // 允许文件类型 allowed := setupFormValue(data, "allowed_file_types", "*") var types []string for _, item := range strings.Split(allowed, ",") { if item = strings.TrimSpace(item); item != "" { types = append(types, item) } } if len(types) == 0 { types = []string{"*"} } out["allowed_file_types"] = types return out, "" } // parseSetupBool 解析表单布尔(缺省 default;"1"/"true"/"on"/"yes" 为真)。 func parseSetupBool(data map[string]any, key string, def bool) bool { v, ok := data[key] if !ok { return def } switch s := v.(type) { case string: switch strings.ToLower(strings.TrimSpace(s)) { case "1", "true", "on", "yes": return true case "0", "false", "off", "no", "": return false } case float64: return s != 0 case bool: return s } return def } // buildSetupPage 初始化向导页面(简洁中文表单)。 func buildSetupPage(errMsg string) string { errBlock := "" if errMsg != "" { errBlock = `
` + htmlEscape(errMsg) + `
` } return ` 初始化 文件快传

初始化 文件快传

首次配置管理员密码、上传限制和取件策略,后续可在后台调整。

` + errBlock + `
` } // buildSetupSuccessPage 初始化完成页。 func buildSetupSuccessPage() string { return ` 初始化完成

初始化完成

管理员密码已设置,请使用刚才的密码登录后台。

进入后台
` } // htmlEscape HTML 转义(错误信息拼接用)。 func htmlEscape(s string) string { r := strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, "'") return r.Replace(s) }