diff --git a/cmd/reposync/reposync.go b/cmd/reposync/reposync.go index 07ebf7e..ab7d885 100644 --- a/cmd/reposync/reposync.go +++ b/cmd/reposync/reposync.go @@ -40,6 +40,7 @@ type Config struct { CommentToTask string PreCommand string PostCommand string + RepoName string } func Run(args []string) { @@ -66,6 +67,7 @@ func Run(args []string) { fs.StringVar(&cfg.CommentToTask, "commenttotask", "false", "Compatible with QL format script comment parsing (true/false)") fs.StringVar(&cfg.PreCommand, "pre-command", "", "Default pre-command for discovered tasks") fs.StringVar(&cfg.PostCommand, "post-command", "", "Default post-command for discovered tasks") + fs.StringVar(&cfg.RepoName, "repo-name", "", "Custom repository directory name") printHelp := func() { fmt.Fprintf(os.Stderr, "\n白虎面板仓库同步工具 (Reposync)\n\n") @@ -162,7 +164,13 @@ func Run(args []string) { func getActualRepoDir(cfg Config) string { if cfg.SourceType == "git" { - repoName := utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch) + repoName := cfg.RepoName + if repoName == "" { + repoName = utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch) + } + if repoName == "." { + return cfg.TargetPath + } return filepath.Join(cfg.TargetPath, repoName) } return cfg.TargetPath @@ -208,10 +216,17 @@ func syncGit(cfg Config) { gitDir := filepath.Join(dest, ".git") if isDir(dest) && !pathExists(gitDir) { - repoName := utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch) - dest = filepath.Join(dest, repoName) - fmt.Printf("目标路径自动追加仓库名: %s\n", dest) - gitDir = filepath.Join(dest, ".git") + repoName := cfg.RepoName + if repoName == "" { + repoName = utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch) + } + if repoName != "." { + dest = filepath.Join(dest, repoName) + fmt.Printf("目标路径自动追加仓库名: %s\n", dest) + gitDir = filepath.Join(dest, ".git") + } else { + fmt.Printf("目标路径使用当前目录 (不追加仓库名): %s\n", dest) + } } restore := preserve(dest, cfg.WhitelistPaths) diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 8690800..3d75552 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -56,6 +56,49 @@ func resolveWorkDir(workDir string) string { } return absPath } + +// isValidDirName 校验目录名是否合法 +func isValidDirName(dirName string) bool { + if strings.Contains(dirName, "/") || strings.Contains(dirName, "\\") || strings.Contains(dirName, "..") { + return false + } + for _, ch := range dirName { + if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '.') { + return false + } + } + return true +} + +// getRepoPhysicalPath 计算仓库任务的最终物理绝对路径 +func getRepoPhysicalPath(targetPath, dirName, sourceURL, branch string) string { + if dirName == "." { + return "" // 如果不追加目录,此逻辑不负责判断其根目录(共享的 scripts 目录) + } + finalDirName := dirName + if finalDirName == "" { + finalDirName = utils.GetRepoIdentifier(sourceURL, branch) + } + if finalDirName == "" { + return "" + } + + basePath := targetPath + if basePath == "" || basePath == constant.ScriptsDirPlaceholder { + basePath = constant.ScriptsWorkDir + } else if strings.HasPrefix(basePath, constant.ScriptsDirPlaceholder) { + basePath = filepath.Join(constant.ScriptsWorkDir, strings.TrimPrefix(basePath, constant.ScriptsDirPlaceholder)) + } else if !filepath.IsAbs(basePath) { + basePath = filepath.Join(constant.ScriptsWorkDir, basePath) + } + + fullPath := filepath.Join(basePath, finalDirName) + absPath, err := filepath.Abs(fullPath) + if err != nil { + return "" + } + return absPath +} // CreateTask 创建任务 // @Summary 创建任务 // @Description 创建一个新的任务 @@ -98,11 +141,37 @@ func (tc *TaskController) CreateTask(c *gin.Context) { // 如果是仓库同步任务,根据 URL 生成 SourceID 用于去重 if req.Type == constant.TaskTypeRepo && req.Config != "" { var repoCfg struct { - SourceURL string `json:"source_url"` - Branch string `json:"branch"` + SourceURL string `json:"source_url"` + Branch string `json:"branch"` + DirName string `json:"dir_name"` + TargetPath string `json:"target_path"` } if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { - sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) + if repoCfg.DirName != "" && repoCfg.DirName != "." { + if !isValidDirName(repoCfg.DirName) { + utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑") + return + } + } + + // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame + if repoCfg.DirName != "" && repoCfg.DirName != "." { + sourceID = "repo_" + repoCfg.DirName + } else { + sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) + } + + // 如果是全新任务,校验物理目录是否存在 + existingTask := tc.taskService.GetTaskBySourceID(sourceID) + if existingTask == nil { + newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch) + if newAbsPath != "" { + if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() { + utils.BadRequest(c, "本地已存在同名仓库文件夹,请更换自定义目录名或清理残留文件") + return + } + } + } } } @@ -262,11 +331,49 @@ func (tc *TaskController) UpdateTask(c *gin.Context) { var sourceID string if req.Type == constant.TaskTypeRepo && req.Config != "" { var repoCfg struct { - SourceURL string `json:"source_url"` - Branch string `json:"branch"` + SourceURL string `json:"source_url"` + Branch string `json:"branch"` + DirName string `json:"dir_name"` + TargetPath string `json:"target_path"` } if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" { - sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) + if repoCfg.DirName != "" && repoCfg.DirName != "." { + if !isValidDirName(repoCfg.DirName) { + utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑") + return + } + } + + // 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame + if repoCfg.DirName != "" && repoCfg.DirName != "." { + sourceID = "repo_" + repoCfg.DirName + } else { + sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch) + } + + // 计算新的物理路径 + newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch) + + var oldAbsPath string + if oldTask != nil && oldTask.Type == constant.TaskTypeRepo && oldTask.Config != "" { + var oldCfg struct { + SourceURL string `json:"source_url"` + Branch string `json:"branch"` + DirName string `json:"dir_name"` + TargetPath string `json:"target_path"` + } + if json.Unmarshal([]byte(oldTask.Config), &oldCfg) == nil { + oldAbsPath = getRepoPhysicalPath(oldCfg.TargetPath, oldCfg.DirName, oldCfg.SourceURL, oldCfg.Branch) + } + } + + // 如果路径发生了改变(或者是个全新计算的路径),并且新路径已存在,则报错拦截 + if newAbsPath != "" && newAbsPath != oldAbsPath { + if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() { + utils.BadRequest(c, "目标目录在本地已存在同名文件夹,请更换目录名或清理残留文件") + return + } + } } } else if oldTask != nil { sourceID = oldTask.SourceID diff --git a/internal/models/task.go b/internal/models/task.go index 1389384..ce0fdfe 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -59,6 +59,7 @@ type RepoConfig struct { AutoAddCron bool `json:"auto_add_cron"` // 自动解析脚本注释添加定时任务 CommentToTask string `json:"commenttotask"` // 兼容 QL 格式任务脚本注释解析 RepoSource string `json:"repo_source"` // 仓库来源,如果是选择了这个 ql 导入的仓库,= ql + DirName string `json:"dir_name"` // 自定义仓库目录名 (可填 "." 表示不追加子目录) } // TaskConfig 任务配置 RepoConfig+TaskConfig=task.config diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index ea47359..b58d7b1 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -1267,6 +1267,9 @@ func BuildRepoCommand(task *models.Task) (string, string) { if config.Extensions != "" { args = append(args, "--extensions", config.Extensions) } + if config.DirName != "" { + args = append(args, "--repo-name", config.DirName) + } if string(task.PreCommand) != "" { args = append(args, "--pre-command", string(task.PreCommand)) } diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 8e773d4..4821909 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -487,6 +487,7 @@ export interface RepoConfig { commenttotask?: string concurrency?: number repo_source?: string + dir_name?: string } export interface ExecutionResult { diff --git a/web/src/utils/repo-parser.ts b/web/src/utils/repo-parser.ts index 1567082..cfd820a 100644 --- a/web/src/utils/repo-parser.ts +++ b/web/src/utils/repo-parser.ts @@ -126,6 +126,9 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null { console.error('Parse task-langs failed', e) } break + case '--repo-name': + repoConfig.dir_name = value + break case '--pre-command': task.pre_command = value break @@ -213,6 +216,9 @@ export function generateBaihuCommand(task: Task): string { if (config.branch) { args.push('--branch', config.branch) } + if (config.dir_name) { + args.push('--repo-name', config.dir_name) + } if (config.sparse_path) { args.push('--path', config.sparse_path) } diff --git a/web/src/views/tasks/RepoDialog.vue b/web/src/views/tasks/RepoDialog.vue index 47fd429..a6763ce 100644 --- a/web/src/views/tasks/RepoDialog.vue +++ b/web/src/views/tasks/RepoDialog.vue @@ -62,7 +62,8 @@ const repoConfig = ref({ commenttotask: 'false', concurrency: 1, repo_source: '', - proxy: '' + proxy: '', + dir_name: '' }) const allAgents = ref([]) @@ -98,6 +99,7 @@ function exportBaihuCommand() { if (repoConfig.value.source_url) parts.push(`--source-url "${repoConfig.value.source_url}"`) if (repoConfig.value.target_path) parts.push(`--target-path "${repoConfig.value.target_path}"`) if (repoConfig.value.branch) parts.push(`--branch "${repoConfig.value.branch}"`) + if (repoConfig.value.dir_name) parts.push(`--repo-name "${repoConfig.value.dir_name}"`) if (repoConfig.value.sparse_path) parts.push(`--path "${repoConfig.value.sparse_path}"`) if (repoConfig.value.single_file) parts.push(`--single-file`) if (repoConfig.value.proxy && repoConfig.value.proxy !== 'none') parts.push(`--proxy ${repoConfig.value.proxy}`) @@ -228,7 +230,8 @@ watch(() => props.open, async (val: boolean) => { auto_add_cron: false, commenttotask: 'false', concurrency: 1, - repo_source: '' + repo_source: '', + dir_name: '' } const configStr = props.task?.config if (configStr) { @@ -412,6 +415,13 @@ async function save() { + +
+ +
+ +
+