{"record":{"id":"944b01d83ffd7ecd","repo":"vxcontrol/pentagi","slug":"w-cannot-copy-directory-into-itself","errorCode":null,"errorMessage":"%w: cannot copy directory into itself","messagePattern":"%w: cannot copy directory into itself","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"backend/pkg/server/services/resources.go","lineNumber":1564,"sourceCode":"\n\tentry := convertResource(newRec)\n\tif exists {\n\t\tresult.Updated = append(result.Updated, entry)\n\t} else {\n\t\tresult.Added = append(result.Added, entry)\n\t}\n\treturn result, nil\n}\n\nfunc (s *ResourceService) copyDirResource(\n\ttx *gorm.DB,\n\tuid uint64,\n\tsrcPath, dstPath string,\n\tforce bool,\n) (copyResourceResult, error) {\n\tresult := copyResourceResult{}\n\tif resources.PathHasPrefix(dstPath, srcPath) {\n\t\treturn result, fmt.Errorf(\"%w: cannot copy directory into itself\", errResourceInvalid)\n\t}\n\n\tcreatedRoot, deletedRoot, orphanHashes, err := ensureResourceDirs(tx, uid, dstPath, force)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\tresult.Added = append(result.Added, convertResources(createdRoot)...)\n\tresult.Deleted = append(result.Deleted, convertResources(deletedRoot)...)\n\tresult.OrphanHashes = append(result.OrphanHashes, orphanHashes...)\n\n\tdest, destExists, err := findResourceByPath(tx, uid, dstPath)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\tif destExists && !dest.IsDir {\n\t\treturn result, errResourceConflict\n\t}\n\tif destExists && !force && len(createdRoot) == 0 {","sourceCodeStart":1546,"sourceCodeEnd":1582,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/server/services/resources.go#L1546-L1582","documentation":"copyDirResource guards against copying a directory into itself or one of its descendants, which would be infinitely recursive and corrupt the path tree. It returns errResourceInvalid (wrappable as a 400-class error) when the destination path equals or is nested under the source path.","triggerScenarios":"Calling the copy resource API with dst path == src path, or dst = src + \"/sub/...\", e.g. copying directory \"/proj\" to \"/proj/backup\" or \"/a/b\" into \"/a/b/c\".","commonSituations":"Frontend bug where the destination picker lets the user select the source folder or its child; scripted API calls computing dst by string concatenation without checking containment.","solutions":["Choose a destination outside the source directory subtree","Validate client-side that dst is not prefixed by src before calling the API","If same-named sibling is wanted, use a path that is not a prefix match (e.g. \"/proj-backup\" instead of \"/proj/backup\")"],"exampleFix":"// before\ncopyDir(uid, \"/proj\", \"/proj/backup\") // error\n// after\ncopyDir(uid, \"/proj\", \"/backups/proj\") // ok\nif PathHasPrefix(dst, src) { return ErrInvalidDest }","handlingStrategy":"validation","validationCode":"func isCopyIntoSelf(src, dst string) bool {\n    return dst == src || strings.HasPrefix(dst, src+\"/\")\n}\nif isCopyIntoSelf(srcPath, dstPath) { return errors.New(\"destination inside source\") }","typeGuard":"func validCopyDest(src, dst string) bool {\n    return dst != \"\" && !strings.HasPrefix(dst, src+\"/\") && dst != src\n}","tryCatchPattern":"if err := copyDir(uid, src, dst, force); err != nil {\n    if errors.Is(err, errResourceInvalid) {\n        return fmt.Errorf(\"bad destination %q: %w\", dst, err)\n    }\n    return err\n}","preventionTips":["Validate dst is not equal to or nested under src in the UI and API client","Never build dst by naive string concatenation of src","Add unit tests for prefix-edge cases (src=/a, dst=/ab must be allowed)"],"tags":["validation","copy","resources","path"],"backgroundTag":"copy-into-itself","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}