diff --git a/backend/cmd/main.go b/backend/cmd/main.go index bc54df1..bb3df35 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -265,6 +265,9 @@ func main() { scriptsGroup.POST("/folder", scriptManageHandlers.CreateFolder) scriptsGroup.DELETE("/folder", scriptManageHandlers.DeleteFolder) + // Rename script or folder + scriptsGroup.POST("/rename", scriptManageHandlers.Rename) + // Get script by path scriptsGroup.GET("/by-path", scriptManageHandlers.GetScriptByPath) } diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 80f8092..bad5898 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -2018,6 +2018,73 @@ const docTemplate = `{ } } }, + "/scripts/rename": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Renames a single script or all scripts under a folder prefix", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "scripts" + ], + "summary": "Rename script or folder", + "parameters": [ + { + "description": "Rename request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.RenameRequest" + } + } + ], + "responses": { + "200": { + "description": "Rename result with count of renamed scripts", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/scripts/run": { "post": { "security": [ @@ -2798,6 +2865,23 @@ const docTemplate = `{ } } }, + "internal_handlers.RenameRequest": { + "type": "object", + "required": [ + "new_path", + "old_path" + ], + "properties": { + "new_path": { + "type": "string", + "example": "deploy/nginx-v2" + }, + "old_path": { + "type": "string", + "example": "deploy/nginx" + } + } + }, "internal_handlers.RunScriptIn": { "type": "object", "required": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 3ab94c9..c7416e0 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -2007,6 +2007,73 @@ } } }, + "/scripts/rename": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Renames a single script or all scripts under a folder prefix", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "scripts" + ], + "summary": "Rename script or folder", + "parameters": [ + { + "description": "Rename request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.RenameRequest" + } + } + ], + "responses": { + "200": { + "description": "Rename result with count of renamed scripts", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/scripts/run": { "post": { "security": [ @@ -2787,6 +2854,23 @@ } } }, + "internal_handlers.RenameRequest": { + "type": "object", + "required": [ + "new_path", + "old_path" + ], + "properties": { + "new_path": { + "type": "string", + "example": "deploy/nginx-v2" + }, + "old_path": { + "type": "string", + "example": "deploy/nginx" + } + } + }, "internal_handlers.RunScriptIn": { "type": "object", "required": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index dc725b8..297cda4 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -477,6 +477,18 @@ definitions: client_cert: type: string type: object + internal_handlers.RenameRequest: + properties: + new_path: + example: deploy/nginx-v2 + type: string + old_path: + example: deploy/nginx + type: string + required: + - new_path + - old_path + type: object internal_handlers.RunScriptIn: properties: agent_id: @@ -1809,6 +1821,49 @@ paths: summary: Update interpreter tags: - scripts + /scripts/rename: + post: + consumes: + - application/json + description: Renames a single script or all scripts under a folder prefix + parameters: + - description: Rename request + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.RenameRequest' + produces: + - application/json + responses: + "200": + description: Rename result with count of renamed scripts + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "409": + description: Conflict + schema: + additionalProperties: + type: string + type: object + security: + - Bearer: [] + summary: Rename script or folder + tags: + - scripts /scripts/run: post: consumes: diff --git a/backend/internal/handlers/scripts_manage.go b/backend/internal/handlers/scripts_manage.go index 1da9540..705a0dd 100644 --- a/backend/internal/handlers/scripts_manage.go +++ b/backend/internal/handlers/scripts_manage.go @@ -280,6 +280,96 @@ type DeleteFolderRequest struct { Path string `json:"path" binding:"required" example:"deploy/nginx" description:"Folder path to delete"` } +// RenameRequest is the request body for renaming a script or folder. +type RenameRequest struct { + OldPath string `json:"old_path" binding:"required" example:"deploy/nginx" description:"Current path"` + NewPath string `json:"new_path" binding:"required" example:"deploy/nginx-v2" description:"New path"` +} + +// Rename renames a script or all scripts under a folder path. +// @Summary Rename script or folder +// @Description Renames a single script or all scripts under a folder prefix +// @Tags scripts +// @Accept json +// @Produce json +// @Param body body RenameRequest true "Rename request" +// @Success 200 {object} map[string]interface{} "Rename result with count of renamed scripts" +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 409 {object} map[string]string +// @Security Bearer +// @Router /scripts/rename [post] +func (sh *ScriptHandlersGroup) Rename(c *gin.Context) { + var req RenameRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } + + // Validate new path + if err := validateScriptPath(req.NewPath); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid new path: %v", err)}) + return + } + + // Validate old path + if err := validateScriptPath(req.OldPath); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid old path: %v", err)}) + return + } + + // Get all scripts + allScripts, err := sh.svc.Repo.ListScripts() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list scripts"}) + return + } + + // Find scripts to rename: exact match or folder prefix + prefix := req.OldPath + "/" + var toRename []repository.Script + for _, script := range allScripts { + if script.Path == req.OldPath || strings.HasPrefix(script.Path, prefix) { + toRename = append(toRename, script) + } + } + + if len(toRename) == 0 { + c.JSON(http.StatusNotFound, gin.H{"error": "no scripts found with this path"}) + return + } + + // Rename each script + renamedCount := 0 + for _, script := range toRename { + newPath := req.NewPath + strings.TrimPrefix(script.Path, req.OldPath) + + // Check if new path already exists (excluding the scripts we're renaming) + for _, existing := range allScripts { + if existing.ID != script.ID && existing.Path == newPath { + c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("path '%s' already exists", newPath)}) + return + } + } + + _, err := sh.svc.Repo.UpdateScript(script.ID, repository.ScriptUpdate{ + Path: &newPath, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to rename %s: %v", script.Path, err)}) + return + } + renamedCount++ + } + + c.JSON(http.StatusOK, gin.H{ + "message": "renamed", + "old_path": req.OldPath, + "new_path": req.NewPath, + "renamed_count": renamedCount, + }) +} + // CreateFolder creates a virtual folder in the script tree. // @Summary Create folder // @Description Creates a virtual folder by creating a placeholder script with the folder path