项目文件夹

文件
2026-07-14 10:24:10 +08:00

337 行
9.9 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package main
import (
"fmt"
"os"
"strings"
"text/template"
"time"
"golang.org/x/text/cases"
"golang.org/x/text/language"
xlgo "github.com/EthanCodeCraft/xlgo-core"
)
func createProject(name string) error {
// P1 #21:校验项目名,拒绝路径穿越与非法 Go 包名。
if err := validateProjectName(name); err != nil {
return err
}
// #nosec G703 -- name was validated by validateProjectName: no path separators, no "..", no leading dot, Go identifier only.
if _, err := os.Stat(name); !os.IsNotExist(err) {
return fmt.Errorf("目录 %s 已存在", name)
}
// 解析 --template 与 --module 参数(默认 template=api
tmplName := "api"
module := name
args := os.Args[3:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "--template", "-t":
if i+1 >= len(args) {
return fmt.Errorf("%s 缺少参数值", args[i])
}
tmplName = args[i+1]
i++
case "--module", "-m":
if i+1 >= len(args) {
return fmt.Errorf("%s 缺少参数值", args[i])
}
module = args[i+1]
i++
default:
// P1 #21:未知参数显式报错,不再静默忽略。
return fmt.Errorf("未知参数: %s", args[i])
}
}
// P1 #21:校验 module 路径,避免模板元字符 {{ }} 经 Sprintf 进 go.mod 后触发 Parse 报错。
if err := validateModulePath(module); err != nil {
return err
}
// 校验模板名
switch tmplName {
case "minimal", "api", "fullstack":
// ok
default:
return fmt.Errorf("未知模板: %s(可选: minimal / api / fullstack", tmplName)
}
// minimal 模板目录结构最小化;api/fullstack 含完整分层目录
var dirs []string
dirs = append(dirs, name, name+"/public", name+"/logs")
if tmplName != "minimal" {
dirs = append(dirs,
name+"/config",
name+"/handler",
name+"/model",
name+"/repository",
name+"/service",
name+"/middleware",
)
}
for _, dir := range dirs {
// #nosec G301,G703 -- dir is built from validated project name plus fixed scaffold subdirectories; 0755 is intentional for generated project dirs.
if err := os.MkdirAll(dir, 0755); err != nil {
// #nosec G703 -- name was validated by validateProjectName and is the scaffold root being rolled back.
_ = os.RemoveAll(name) // P1 #21:清理半成品
return fmt.Errorf("创建目录失败: %w", err)
}
}
caser := cases.Title(language.English)
data := TemplateData{
Package: caser.String(name),
Name: caser.String(name),
NameLower: strings.ToLower(name),
Module: module,
Year: time.Now().Year(),
}
// 按模板选择 main.go 与 config.yaml
var mainTmpl, configTmpl string
switch tmplName {
case "minimal":
mainTmpl, configTmpl = templates.MainMinimal, templates.ConfigMinimal
case "fullstack":
mainTmpl, configTmpl = templates.MainFull, templates.ConfigFull
default: // api
mainTmpl, configTmpl = templates.Main, templates.Config
}
// 创建文件
files := map[string]string{
name + "/main.go": mainTmpl,
name + "/config.yaml": configTmpl,
name + "/go.mod": fmt.Sprintf(templates.GoMod, module, xlgo.Version),
name + "/Makefile": templates.Makefile,
name + "/.gitignore": templates.Gitignore,
}
// api/fullstack 模板带示例 handler
if tmplName != "minimal" {
files[name+"/handler/home.go"] = templates.Handler
}
for path, content := range files {
if err := renderTemplateFile(path, content, data); err != nil {
// #nosec G703 -- name was validated by validateProjectName and is the scaffold root being rolled back.
_ = os.RemoveAll(name) // P1 #21:部分失败回滚,避免留下半成品项目
return err
}
}
fmt.Printf("✓ 项目 %s 创建成功(模板: %s\n", name, tmplName)
fmt.Println("\n下一步:")
fmt.Printf(" cd %s\n", name)
fmt.Println(" go mod tidy")
fmt.Println(" go run main.go")
return nil
}
// renderTemplateFile 解析并渲染单个模板文件,每次调用显式关闭句柄
// P1 #21:不在循环内 defer 累积句柄,且 Close 错误纳入返回)。
func renderTemplateFile(path, content string, data TemplateData) (retErr error) {
tmpl, err := template.New(path).Parse(content)
if err != nil {
return fmt.Errorf("解析模板 %s 失败: %w", path, err)
}
// #nosec G304 -- path is from createProject's files map built from validated project name plus fixed filenames.
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("创建文件 %s 失败: %w", path, err)
}
defer func() {
if cerr := file.Close(); cerr != nil && retErr == nil {
retErr = fmt.Errorf("关闭文件 %s 失败: %w", path, cerr)
}
}()
if err := tmpl.Execute(file, data); err != nil {
return fmt.Errorf("写入文件 %s 失败: %w", path, err)
}
return nil
}
// validateProjectName 校验项目名(P1 #21):非空、无路径分隔符/.. /前导点,
// 且可派生为合法 Go 包名(须以字母开头,仅含字母/数字/下划线)。
func validateProjectName(name string) error {
if name == "" {
return fmt.Errorf("项目名不能为空")
}
if strings.ContainsAny(name, `/\`) || strings.Contains(name, "..") {
return fmt.Errorf("项目名不能包含路径分隔符或 ..(防止在预期目录外创建文件): %q", name)
}
if strings.HasPrefix(name, ".") {
return fmt.Errorf("项目名不能以 . 开头: %q", name)
}
if !isValidGoIdentifier(name) {
return fmt.Errorf("项目名 %q 无法生成合法 Go 包名:须以字母开头,仅含字母、数字、下划线", name)
}
return nil
}
// isValidGoIdentifier 判断 s 是否为脚手架接受的 ASCII 标识符:字母开头,后续允许字母、数字、下划线。
func isValidGoIdentifier(s string) bool {
if s == "" {
return false
}
for i, r := range s {
isLetter := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
isNum := r >= '0' && r <= '9'
if i == 0 {
if !isLetter {
return false
}
} else if !isLetter && !isNum && r != '_' {
return false
}
}
return true
}
// validateModulePath 校验 --moduleP1 #21):非空、不含模板元字符与空白。
func validateModulePath(module string) error {
if module == "" {
return fmt.Errorf("模块路径不能为空")
}
if strings.Contains(module, "{{") || strings.Contains(module, "}}") {
return fmt.Errorf("模块路径不能包含模板元字符 {{ }}: %q", module)
}
if strings.ContainsAny(module, " \t\r\n") {
return fmt.Errorf("模块路径不能包含空白字符: %q", module)
}
return nil
}
func makeFile(fileType, name string) error {
if err := validateMakeName(name); err != nil {
return err
}
name = strings.ToLower(name)
nameTitle := makeNameTitle(name)
switch fileType {
case "handler":
return createHandler(name, nameTitle)
case "repository":
return createRepository(name, nameTitle)
case "model":
return createModel(name, nameTitle)
case "service":
return createService(name, nameTitle)
default:
return fmt.Errorf("未知类型: %s(可用类型: handler, repository, model, service", fileType)
}
}
// validateMakeName 校验 xlgo make 的资源名,避免路径穿越或生成不可编译的 Go 代码。
func validateMakeName(name string) error {
if name == "" {
return fmt.Errorf("名称不能为空")
}
if strings.ContainsAny(name, `/\`) || strings.Contains(name, "..") {
return fmt.Errorf("名称不能包含路径分隔符或 ..: %q", name)
}
if strings.HasPrefix(name, ".") {
return fmt.Errorf("名称不能以 . 开头: %q", name)
}
if !isValidGoIdentifier(name) {
return fmt.Errorf("名称 %q 必须是合法标识符:须以字母开头,仅含字母、数字、下划线", name)
}
return nil
}
// makeNameTitle 把 snake_case 名称转为导出的 CamelCase 类型名。
func makeNameTitle(name string) string {
caser := cases.Title(language.English)
nameTitle := caser.String(strings.ReplaceAll(name, "_", " "))
return strings.ReplaceAll(nameTitle, " ", "")
}
func createHandler(name, nameTitle string) error {
path := fmt.Sprintf("handler/%s.go", name)
if fileExists(path) {
return fmt.Errorf("文件 %s 已存在", path)
}
content := fmt.Sprintf(templates.HandlerMake,
nameTitle, name, nameTitle,
nameTitle, name, nameTitle, nameTitle, nameTitle,
name, nameTitle, name, nameTitle,
nameTitle, name, name, nameTitle,
nameTitle, name, name, nameTitle,
nameTitle, name, name, nameTitle,
nameTitle, name, name, nameTitle,
)
content = replaceModuleImports(content)
if err := writeFile(path, content); err != nil {
return err
}
fmt.Printf("✓ 创建处理器: %s\n", path)
return nil
}
func createRepository(name, nameTitle string) error {
path := fmt.Sprintf("repository/%s_repository.go", name)
if fileExists(path) {
return fmt.Errorf("文件 %s 已存在", path)
}
content := fmt.Sprintf(templates.RepositoryMake,
nameTitle, name, nameTitle, nameTitle,
nameTitle, name, nameTitle, nameTitle, nameTitle, nameTitle,
nameTitle, nameTitle,
)
content = replaceModuleImports(content)
if err := writeFile(path, content); err != nil {
return err
}
fmt.Printf("✓ 创建仓库: %s\n", path)
return nil
}
func createModel(name, nameTitle string) error {
path := fmt.Sprintf("model/%s.go", name)
if fileExists(path) {
return fmt.Errorf("文件 %s 已存在", path)
}
content := fmt.Sprintf(templates.ModelMake,
nameTitle, name, nameTitle, nameTitle, name,
)
if err := writeFile(path, content); err != nil {
return err
}
fmt.Printf("✓ 创建模型: %s\n", path)
return nil
}
func createService(name, nameTitle string) error {
path := fmt.Sprintf("service/%s_service.go", name)
if fileExists(path) {
return fmt.Errorf("文件 %s 已存在", path)
}
content := fmt.Sprintf(templates.ServiceMake,
nameTitle, name, nameTitle, nameTitle,
nameTitle, name, nameTitle, nameTitle, nameTitle, nameTitle, nameTitle,
nameTitle, nameTitle,
nameTitle, nameTitle,
nameTitle, nameTitle,
nameTitle, nameTitle,
)
content = replaceModuleImports(content)
if err := writeFile(path, content); err != nil {
return err
}
fmt.Printf("✓ 创建服务: %s\n", path)
return nil
}