diff --git a/package-lock.json b/package-lock.json index 079600e51..0d82ed002 100644 --- a/package-lock.json +++ b/package-lock.json @@ -359,28 +359,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/core/node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.654.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.654.0.tgz", @@ -1355,28 +1333,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/@aws-sdk/region-config-resolver": { "version": "3.654.0", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.654.0.tgz", @@ -1841,24 +1797,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.26.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -3185,9 +3143,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3196,13 +3155,14 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -3250,9 +3210,10 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -8904,9 +8865,10 @@ } }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -8925,6 +8887,17 @@ "js-md4": "^0.3.2" } }, + "node_modules/axios-ntlm/node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -10996,6 +10969,20 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/config-file-ts/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -13147,6 +13134,28 @@ "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "license": "BSD-3-Clause" }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -24155,16 +24164,17 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/uc.micro": { @@ -25122,6 +25132,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", + "license": "MIT", "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", @@ -26223,26 +26234,15 @@ "node": ">=18.0.0" } }, - "packages/bruno-cli/node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], + "packages/bruno-cli/node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "license": "MIT", "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "packages/bruno-cli/node_modules/fs-extra": { @@ -26274,19 +26274,6 @@ "typescript": "^4.8.4" } }, - "packages/bruno-common/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "packages/bruno-electron": { "name": "bruno", "version": "v1.38.1", @@ -27364,26 +27351,15 @@ "node": ">=18.0.0" } }, - "packages/bruno-electron/node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], + "packages/bruno-electron/node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "license": "MIT", "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "packages/bruno-electron/node_modules/fs-extra": { @@ -27469,19 +27445,6 @@ "loose-envify": "^1.1.0" } }, - "packages/bruno-graphql-docs/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "packages/bruno-js": { "name": "@usebruno/js", "version": "0.12.0", @@ -27522,6 +27485,17 @@ "@usebruno/vm2": "^3.9.13" } }, + "packages/bruno-js/node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-js/node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -27566,19 +27540,6 @@ "typescript": "^4.8.4" } }, - "packages/bruno-query/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "packages/bruno-schema": { "name": "@usebruno/schema", "version": "0.7.0", @@ -27626,10 +27587,22 @@ "multer": "^1.4.5-lts.1" } }, + "packages/bruno-tests/node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-tests/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -27638,6 +27611,7 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -27680,22 +27654,18 @@ } }, "packages/bruno-tests/node_modules/fast-xml-parser": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz", - "integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz", + "integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.0.5" }, "bin": { "fxparser": "src/cli/cli.js" @@ -27704,12 +27674,14 @@ "packages/bruno-tests/node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "packages/bruno-tests/node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -27720,6 +27692,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "packages/bruno-tests/node_modules/strnum": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz", + "integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "packages/bruno-toml": { "name": "@usebruno/toml", "version": "0.1.0", diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js index ccfac9046..0bc91b5c3 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js @@ -8,9 +8,7 @@ import { useState } from 'react'; import StyledWrapper from './StyledWrapper'; import { useRef } from 'react'; -import path from 'path'; -import slash from 'utils/common/slash'; -import { isWindowsOS } from 'utils/common/platform'; +import path from 'utils/common/path'; const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => { const certFilePathInputRef = useRef(); @@ -70,12 +68,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => { const getFile = (e) => { const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]); if (filePath) { - let relativePath; - if (isWindowsOS()) { - relativePath = slash(path.win32.relative(root, filePath)); - } else { - relativePath = path.posix.relative(root, filePath); - } + let relativePath = path.relative(root, filePath); formik.setFieldValue(e.name, relativePath); } }; @@ -109,23 +102,23 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {

Add Client Certificate

@@ -198,9 +191,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
- {path.basename(slash(formik.values.certFilePath))} + {path.basename(formik.values.certFilePath)}
{
- {path.basename(slash(formik.values.keyFilePath))} + {path.basename(formik.values.keyFilePath)}
{
- {path.basename(slash(formik.values.pfxFilePath))} + {path.basename(formik.values.pfxFilePath)}
{ const dispatch = useDispatch(); @@ -23,7 +24,11 @@ const CreateEnvironment = ({ collection, onClose }) => { validationSchema: Yup.object({ name: Yup.string() .min(1, 'Must be at least 1 character') - .max(50, 'Must be 50 characters or less') + .max(255, 'Must be 255 characters or less') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) .required('Name is required') .test('duplicate-name', 'Environment already exists', validateEnvironmentName) }), diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js index 3ebcadca1..fee403d8a 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js @@ -6,6 +6,7 @@ import { useFormik } from 'formik'; import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import * as Yup from 'yup'; import { useDispatch } from 'react-redux'; +import { validateName, validateNameError } from 'utils/common/regex'; const RenameEnvironment = ({ onClose, environment, collection }) => { const dispatch = useDispatch(); @@ -18,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment, collection }) => { validationSchema: Yup.object({ name: Yup.string() .min(1, 'must be at least 1 character') - .max(50, 'must be 50 characters or less') + .max(255, 'Must be 255 characters or less') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) .required('name is required') }), onSubmit: (values) => { diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index be7d689a3..26969dde3 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -1,10 +1,9 @@ import React from 'react'; -import path from 'path'; +import path from 'utils/common/path'; import { useDispatch } from 'react-redux'; import { browseFiles } from 'providers/ReduxStore/slices/collections/actions'; import { IconX } from '@tabler/icons'; import { isWindowsOS } from 'utils/common/platform'; -import slash from 'utils/common/slash'; const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => { const dispatch = useDispatch(); @@ -27,7 +26,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa const collectionDir = collection.pathname; if (filePath.startsWith(collectionDir)) { - return path.relative(slash(collectionDir), slash(filePath)); + return path.relative(collectionDir, filePath); } return filePath; diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js index 3bf8af65e..d9eb83191 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js @@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux'; import Portal from 'components/Portal'; import Modal from 'components/Modal'; import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; +import { validateName, validateNameError } from 'utils/common/regex'; const CreateEnvironment = ({ onClose }) => { const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments); @@ -25,7 +26,11 @@ const CreateEnvironment = ({ onClose }) => { validationSchema: Yup.object({ name: Yup.string() .min(1, 'Must be at least 1 character') - .max(50, 'Must be 50 characters or less') + .max(255, 'Must be 255 characters or less') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) .required('Name is required') .test('duplicate-name', 'Global Environment already exists', validateEnvironmentName) }), diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js index ff1809383..581abd27c 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js @@ -3,10 +3,10 @@ import Portal from 'components/Portal/index'; import Modal from 'components/Modal/index'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; -import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import * as Yup from 'yup'; import { useDispatch } from 'react-redux'; import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; +import { validateName, validateNameError } from 'utils/common/regex'; const RenameEnvironment = ({ onClose, environment }) => { const dispatch = useDispatch(); @@ -19,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment }) => { validationSchema: Yup.object({ name: Yup.string() .min(1, 'must be at least 1 character') - .max(50, 'must be 50 characters or less') + .max(255, 'Must be 255 characters or less') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) .required('name is required') }), onSubmit: (values) => { diff --git a/packages/bruno-app/src/components/PathDisplay/StyledWrapper.js b/packages/bruno-app/src/components/PathDisplay/StyledWrapper.js new file mode 100644 index 000000000..943a58673 --- /dev/null +++ b/packages/bruno-app/src/components/PathDisplay/StyledWrapper.js @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .path-display { + background: ${(props) => props.theme.requestTabPanel.url.bg}; + border-radius: 4px; + padding: 8px 12px; + + .filename { + color: ${(props) => props.theme.brand}; + font-weight: 500; + min-height: 1.25rem; + } + + .file-extension { + color: ${(props) => props.theme.text}; + opacity: 0.5; + } + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/PathDisplay/index.js b/packages/bruno-app/src/components/PathDisplay/index.js new file mode 100644 index 000000000..78b6f189a --- /dev/null +++ b/packages/bruno-app/src/components/PathDisplay/index.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { IconEdit, IconFolder, IconFile } from '@tabler/icons'; +import path from 'utils/common/path'; +import StyledWrapper from './StyledWrapper'; + +const PathDisplay = ({ + collection, + item, + filename, + extension = '.bru', + showExtension = true, + toggleEditingFilename, + showDirectory = false +}) => { + const relativePath = item?.pathname && path.relative(collection?.pathname, showDirectory ? path.dirname(item?.pathname) : item?.pathname); + const pathSegments = relativePath?.split(path.sep).filter(Boolean); + + return ( + +
+
+ + toggleEditingFilename(true)} + /> +
+
+
+
+ {showExtension ? : } + {collection?.name} +
+ {pathSegments?.length > 0 && pathSegments?.map((segment, index) => ( +
+ / + {segment} +
+ ))} +
+ {collection && /} + + {filename} + {showExtension && filename?.length ? ( + {extension} + ) : null} + +
+
+
+
+
+ ); +}; + +export default PathDisplay; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js index 2867d9841..26054a881 100644 --- a/packages/bruno-app/src/components/Preferences/General/index.js +++ b/packages/bruno-app/src/components/Preferences/General/index.js @@ -6,8 +6,7 @@ import { savePreferences } from 'providers/ReduxStore/slices/app'; import StyledWrapper from './StyledWrapper'; import * as Yup from 'yup'; import toast from 'react-hot-toast'; -import path from 'path'; -import slash from 'utils/common/slash'; +import path from 'utils/common/path'; import { IconTrash } from '@tabler/icons'; const General = ({ close }) => { @@ -134,7 +133,7 @@ const General = ({ close }) => { className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`} > - {path.basename(slash(formik.values.customCaCertificate.filePath))} + {path.basename(formik.values.customCaCertificate.filePath)}
- - - + } {formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
{formik.errors.collectionFolderName}
) : null} diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js index 0bf17603d..37d144735 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; import * as Yup from 'yup'; @@ -6,24 +6,42 @@ import Modal from 'components/Modal'; import { useDispatch } from 'react-redux'; import { isItemAFolder } from 'utils/tabs'; import { cloneItem } from 'providers/ReduxStore/slices/collections/actions'; +import { IconArrowBackUp } from '@tabler/icons'; +import * as path from 'path'; +import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; +import PathDisplay from 'components/PathDisplay/index'; const CloneCollectionItem = ({ collection, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); + const [isEditingFilename, toggleEditingFilename] = useState(false); + const itemName = item?.name; + const itemType = item?.type; + const itemFilename = item?.filename ? path.parse(item?.filename).name : ''; const formik = useFormik({ enableReinitialize: true, initialValues: { - name: item.name + name: itemName, + filename: sanitizeName(itemFilename) }, validationSchema: Yup.object({ name: Yup.string() .min(1, 'must be at least 1 character') - .max(50, 'must be 50 characters or less') + .max(255, 'must be 255 characters or less') + .required('name is required'), + filename: Yup.string() + .min(1, 'must be at least 1 character') + .max(255, 'must be 255 characters or less') .required('name is required') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) + .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)) }), onSubmit: (values) => { - dispatch(cloneItem(values.name, item.uid, collection.uid)) + dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid)) .then(() => { toast.success('Request cloned!'); onClose(); @@ -44,7 +62,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => { return ( { autoCorrect="off" autoCapitalize="off" spellCheck="false" - onChange={formik.handleChange} + onChange={e => { + formik.setFieldValue('name', e.target.value); + !isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value)); + }} value={formik.values.name || ''} /> {formik.touched.name && formik.errors.name ?
{formik.errors.name}
: null}
+ {isEditingFilename ? ( +
+
+ + toggleEditingFilename(false)} + /> +
+
+ + {itemType !== 'folder' && .bru} +
+
+ ) : ( + + )} + {formik.touched.filename && formik.errors.filename ? ( +
{formik.errors.filename}
+ ) : null} ); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js new file mode 100644 index 000000000..1b20e666f --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js @@ -0,0 +1,39 @@ +import React from 'react'; +import Modal from 'components/Modal'; +import * as path from 'path'; + +const CollectionItemInfo = ({ collection, item, onClose }) => { + const { pathname: collectionPathname } = collection; + const { name, filename, pathname, type } = item; + const relativePathname = path.relative(collectionPathname, pathname); + return ( + +
+ + + + + + + + + + + + + + + +
Name :{name}
{type=='folder' ? 'Directory Name' : 'File Name'} :{filename}
Pathname :{relativePathname}
+
+
+ ); +}; + +export default CollectionItemInfo; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 04744b6d8..9d174560d 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -1,46 +1,73 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import Modal from 'components/Modal'; import { useDispatch } from 'react-redux'; import { isItemAFolder } from 'utils/tabs'; import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import path from 'utils/common/path'; +import { IconArrowBackUp } from '@tabler/icons'; +import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import toast from 'react-hot-toast'; import { closeTabs } from 'providers/ReduxStore/slices/tabs'; +import PathDisplay from 'components/PathDisplay'; const RenameCollectionItem = ({ collection, item, onClose }) => { const dispatch = useDispatch(); const isFolder = isItemAFolder(item); const inputRef = useRef(); + const [isEditingFilename, toggleEditingFilename] = useState(false); + const itemName = item?.name; + const itemType = item?.type; + const itemFilename = item?.filename ? path.parse(item?.filename).name : ''; const formik = useFormik({ enableReinitialize: true, initialValues: { - name: item.name + name: itemName, + filename: sanitizeName(itemFilename) }, validationSchema: Yup.object({ name: Yup.string() .min(1, 'must be at least 1 character') - .max(50, 'must be 50 characters or less') + .max(255, 'must be 255 characters or less') + .required('name is required'), + filename: Yup.string() + .min(1, 'must be at least 1 character') + .max(255, 'must be 255 characters or less') .required('name is required') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) + .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)) }), onSubmit: async (values) => { // if there is unsaved changes in the request, // save them before renaming the request + if ((item.name === values.name) && (itemFilename === values.filename)) { + return; + } if (!isFolder && item.draft) { await dispatch(saveRequest(item.uid, collection.uid, true)); } - if (item.name === values.name) { - return; + const { name: newName, filename: newFilename } = values; + try { + let renameConfig = { + itemUid: item.uid, + collectionUid: collection.uid, + }; + renameConfig['newName'] = newName; + if (itemFilename !== newFilename) { + renameConfig['newFilename'] = newFilename; + } + await dispatch(renameItem(renameConfig)); + if (isFolder) { + dispatch(closeTabs({ tabUids: [item.uid] })); + } + onClose(); + } catch (error) { + toast.error(error.message || 'An error occurred while renaming'); } - dispatch(renameItem(values.name, item.uid, collection.uid)) - .then(() => { - isFolder && dispatch(closeTabs({ tabUids: [item.uid] })); - toast.success(isFolder ? 'Folder renamed' : 'Request renamed'); - onClose(); - }) - .catch((err) => { - toast.error(err ? err.message : 'An error occurred while renaming the request'); - }); } }); @@ -54,14 +81,14 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { return ( -
e.preventDefault()}> -
+ {e.preventDefault()}}> +
@@ -75,11 +102,59 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { autoCorrect="off" autoCapitalize="off" spellCheck="false" - onChange={formik.handleChange} + onChange={e => { + formik.setFieldValue('name', e.target.value); + !isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value)); + }} value={formik.values.name || ''} /> {formik.touched.name && formik.errors.name ?
{formik.errors.name}
: null}
+ + {isEditingFilename ? ( +
+
+ + toggleEditingFilename(false)} + /> +
+
+ + {itemType !== 'folder' && .bru} +
+
+ ) : ( + + )} + {formik.touched.filename && formik.errors.filename ? ( +
{formik.errors.filename}
+ ) : null} ); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index a08fac3b7..41a8a9d32 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -23,6 +23,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app'; import toast from 'react-hot-toast'; import StyledWrapper from './StyledWrapper'; import NetworkError from 'components/ResponsePane/NetworkError/index'; +import CollectionItemInfo from './CollectionItemInfo/index'; import { findItemInCollection } from 'utils/collections'; import CollectionItemIcon from './CollectionItemIcon'; import { scrollToTheActiveTab } from 'utils/tabs'; @@ -41,7 +42,7 @@ const CollectionItem = ({ item, collection, searchText }) => { const [newRequestModalOpen, setNewRequestModalOpen] = useState(false); const [newFolderModalOpen, setNewFolderModalOpen] = useState(false); const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false); - + const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false); const hasSearchText = searchText && searchText?.trim()?.length; const itemIsCollapsed = hasSearchText ? false : item.collapsed; @@ -259,6 +260,9 @@ const CollectionItem = ({ item, collection, searchText }) => { {generateCodeItemModalOpen && ( setGenerateCodeItemModalOpen(false)} /> )} + {itemInfoModalOpen && ( + setItemInfoModalOpen(false)} /> + )}
{indents && indents.length @@ -413,6 +417,15 @@ const CollectionItem = ({ item, collection, searchText }) => { Settings
)} +
{ + dropdownTippyRef.current.hide(); + setItemInfoModalOpen(true); + }} + > + Info +
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 50650d5f5..019195c6c 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -5,12 +5,16 @@ import * as Yup from 'yup'; import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions'; import { createCollection } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; -import InfoTip from 'components/InfoTip'; import Modal from 'components/Modal'; +import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; +import PathDisplay from 'components/PathDisplay/index'; +import { useState } from 'react'; +import { IconArrowBackUp } from '@tabler/icons'; const CreateCollection = ({ onClose }) => { const inputRef = useRef(); const dispatch = useDispatch(); + const [isEditingFilename, toggleEditingFilename] = useState(false); const formik = useFormik({ enableReinitialize: true, @@ -22,12 +26,15 @@ const CreateCollection = ({ onClose }) => { validationSchema: Yup.object({ collectionName: Yup.string() .min(1, 'must be at least 1 character') - .max(50, 'must be 50 characters or less') + .max(255, 'must be 255 characters or less') .required('collection name is required'), collectionFolderName: Yup.string() .min(1, 'must be at least 1 character') - .max(50, 'must be 50 characters or less') - .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters') + .max(255, 'must be 255 characters or less') + .test('is-valid-dir-name', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) .required('folder name is required'), collectionLocation: Yup.string().min(1, 'location is required').required('location is required') }), @@ -78,9 +85,7 @@ const CreateCollection = ({ onClose }) => { className="block textbox mt-2 w-full" onChange={(e) => { formik.handleChange(e); - if (formik.values.collectionName === formik.values.collectionFolderName) { - formik.setFieldValue('collectionFolderName', e.target.value); - } + !isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value)); }} autoComplete="off" autoCorrect="off" @@ -116,26 +121,42 @@ const CreateCollection = ({ onClose }) => { Browse
- - - + } {formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
{formik.errors.collectionFolderName}
) : null} diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js index ada38a1cb..3f6c4ccf4 100644 --- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js @@ -1,40 +1,52 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; import * as Yup from 'yup'; import Modal from 'components/Modal'; import { useDispatch } from 'react-redux'; import { newFolder } from 'providers/ReduxStore/slices/collections/actions'; +import { IconArrowBackUp } from '@tabler/icons'; +import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; +import PathDisplay from 'components/PathDisplay'; const NewFolder = ({ collection, item, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); + const [isEditingFilename, toggleEditingFilename] = useState(false); + const formik = useFormik({ enableReinitialize: true, initialValues: { - folderName: '' + folderName: '', + directoryName: '' }, validationSchema: Yup.object({ folderName: Yup.string() .trim() .min(1, 'must be at least 1 character') - .required('name is required') + .required('name is required'), + directoryName: Yup.string() + .trim() + .min(1, 'must be at least 1 character') + .required('foldername is required') + .test('is-valid-folder-name', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) .test({ name: 'folderName', message: 'The folder name "environments" at the root of the collection is reserved in bruno', test: (value) => { - if (item && item.uid) { - return true; - } + if (item?.uid) return true; return value && !value.trim().toLowerCase().includes('environments'); } }) }), onSubmit: (values) => { - dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null)) + dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null)) .then(() => { toast.success('New folder created!'); - onClose() + onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder')); } @@ -49,8 +61,8 @@ const NewFolder = ({ collection, item, onClose }) => { const onSubmit = () => formik.handleSubmit(); return ( - -
e.preventDefault()}> + +
+ + {isEditingFilename ? ( +
+
+ + toggleEditingFilename(false)} + /> +
+
+ +
+
+ ) : ( + + )} + {formik.touched.directoryName && formik.errors.directoryName ? ( +
{formik.errors.directoryName}
+ ) : null}
); diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js index f7d7e914d..872ba2877 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js @@ -1,52 +1,45 @@ import styled from 'styled-components'; -const StyledWrapper = styled.div` - div.method-selector-container { - border: solid 1px ${(props) => props.theme.modal.input.border}; - border-right: none; - background-color: ${(props) => props.theme.modal.input.bg}; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; + const StyledWrapper = styled.div` + div.method-selector-container { + border: solid 1px ${(props) => props.theme.modal.input.border}; + border-right: none; + background-color: ${(props) => props.theme.modal.input.bg}; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + .method-selector { + min-width: 80px; + } + } + div.method-selector-container, + div.input-container { + background-color: ${(props) => props.theme.modal.input.bg}; + height: 2.3rem; + } + div.input-container { + border: solid 1px ${(props) => props.theme.modal.input.border}; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + input { + background-color: ${(props) => props.theme.modal.input.bg}; + outline: none; + box-shadow: none; + &:focus { + outline: none !important; + box-shadow: none !important; + } + } + } + textarea.curl-command { + min-height: 150px; + } + .dropdown { + width: fit-content; + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } + `; - .method-selector { - min-width: 80px; - } - } - - div.method-selector-container, - div.input-container { - background-color: ${(props) => props.theme.modal.input.bg}; - height: 2.3rem; - } - - div.input-container { - border: solid 1px ${(props) => props.theme.modal.input.border}; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; - - input { - background-color: ${(props) => props.theme.modal.input.bg}; - outline: none; - box-shadow: none; - - &:focus { - outline: none !important; - box-shadow: none !important; - } - } - } - - textarea.curl-command { - min-height: 150px; - } - - .dropdown { - width: fit-content; - - .dropdown-item { - padding: 0.2rem 0.6rem !important; - } - } -`; - -export default StyledWrapper; + export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index f95b3efcc..2f201f469 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -10,10 +10,12 @@ import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions' import { addTab } from 'providers/ReduxStore/slices/tabs'; import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector'; import { getDefaultRequestPaneTab } from 'utils/collections'; -import StyledWrapper from './StyledWrapper'; import { getRequestFromCurlCommand } from 'utils/curl'; +import { IconArrowBackUp, IconCaretDown } from '@tabler/icons'; +import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import Dropdown from 'components/Dropdown'; -import { IconCaretDown } from '@tabler/icons'; +import PathDisplay from 'components/PathDisplay'; +import StyledWrapper from './StyledWrapper'; const NewRequest = ({ collection, item, isEphemeral, onClose }) => { const dispatch = useDispatch(); @@ -55,6 +57,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { setCurlRequestTypeDetected(type); }; + const [isEditingFilename, toggleEditingFilename] = useState(false); + const getRequestType = (collectionPresets) => { if (!collectionPresets || !collectionPresets.requestType) { return 'http-request'; @@ -79,6 +83,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { enableReinitialize: true, initialValues: { requestName: '', + filename: '', requestType: getRequestType(collectionPresets), requestUrl: collectionPresets.requestUrl || '', requestMethod: 'GET', @@ -88,15 +93,18 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { requestName: Yup.string() .trim() .min(1, 'must be at least 1 character') - .required('name is required') - .test({ - name: 'requestName', - message: `The request names - collection and folder is reserved in bruno`, - test: (value) => { - const trimmedValue = value ? value.trim().toLowerCase() : ''; - return !['collection', 'folder'].includes(trimmedValue); - } - }), + .max(255, 'must be 255 characters or less') + .required('name is required'), + filename: Yup.string() + .trim() + .min(1, 'must be at least 1 character') + .max(255, 'must be 255 characters or less') + .required('filename is required') + .test('is-valid-filename', function(value) { + const isValid = validateName(value); + return isValid ? true : this.createError({ message: validateNameError(value) }); + }) + .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)), curlCommand: Yup.string().when('requestType', { is: (requestType) => requestType === 'from-curl', then: Yup.string() @@ -116,6 +124,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { newEphemeralHttpRequest({ uid: uid, requestName: values.requestName, + filename: values.filename, requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, @@ -138,6 +147,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { dispatch( newHttpRequest({ requestName: values.requestName, + filename: values.filename, requestType: curlRequestTypeDetected, requestUrl: request.url, requestMethod: request.method, @@ -157,6 +167,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { dispatch( newHttpRequest({ requestName: values.requestName, + filename: values.filename, requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, @@ -221,7 +232,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { return ( -
e.preventDefault()}> + { + if (e.key === 'Enter') { + e.preventDefault(); + formik.handleSubmit(); + } + }} + >
+ {isEditingFilename ? ( +
+
+ + toggleEditingFilename(false)} + /> +
+
+ + .bru +
+
+ ) : ( + + )} + {formik.touched.filename && formik.errors.filename ? ( +
{formik.errors.filename}
+ ) : null} {formik.values.requestType !== 'from-curl' ? ( <>
-
(dispatch, getState) => { const state = getState(); @@ -343,7 +343,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) }); }; -export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => { +export const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); @@ -355,14 +355,14 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS if (!itemUid) { const folderWithSameNameExists = find( collection.items, - (i) => i.type === 'folder' && trim(i.name) === trim(folderName) + (i) => i.type === 'folder' && trim(i.filename) === trim(directoryName) ); if (!folderWithSameNameExists) { - const fullName = `${collection.pathname}${PATH_SEPARATOR}${folderName}`; + const fullName = path.join(collection.pathname, directoryName); const { ipcRenderer } = window; ipcRenderer - .invoke('renderer:new-folder', fullName) + .invoke('renderer:new-folder', fullName, folderName) .then(() => resolve()) .catch((error) => reject(error)); } else { @@ -373,14 +373,14 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS if (currentItem) { const folderWithSameNameExists = find( currentItem.items, - (i) => i.type === 'folder' && trim(i.name) === trim(folderName) + (i) => i.type === 'folder' && trim(i.filename) === trim(directoryName) ); if (!folderWithSameNameExists) { - const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${folderName}`; + const fullName = path.join(currentItem.pathname, directoryName); const { ipcRenderer } = window; ipcRenderer - .invoke('renderer:new-folder', fullName) + .invoke('renderer:new-folder', fullName, folderName) .then(() => resolve()) .catch((error) => reject(error)); } else { @@ -393,8 +393,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS }); }; -// rename item -export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => { +export const renameItem = ({ newName, newFilename, itemUid, collectionUid }) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); @@ -409,22 +408,53 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta return reject(new Error('Unable to locate item')); } - const dirname = getDirectoryName(item.pathname); - - let newPathname = ''; - if (item.type === 'folder') { - newPathname = path.join(dirname, trim(newName)); - } else { - const filename = resolveRequestFilename(newName); - newPathname = path.join(dirname, filename); - } const { ipcRenderer } = window; - ipcRenderer.invoke('renderer:rename-item', slash(item.pathname), newPathname, newName).then(resolve).catch(reject); + const renameName = async () => { + return ipcRenderer.invoke('renderer:rename-item-name', { itemPath: item.pathname, newName }) + .catch((err) => { + toast.error('Failed to rename the item name'); + console.error(err); + throw new Error('Failed to rename the item name'); + }); + }; + + const renameFile = async () => { + const dirname = path.dirname(item.pathname); + let newPath = ''; + if (item.type === 'folder') { + newPath = path.join(dirname, trim(newFilename)); + } else { + const filename = resolveRequestFilename(newFilename); + newPath = path.join(dirname, filename); + } + + return ipcRenderer.invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename }) + .catch((err) => { + toast.error('Failed to rename the file'); + console.error(err); + throw new Error('Failed to rename the file'); + }); + }; + + let renameOperation = null; + if (newName) renameOperation = renameName; + if (newFilename) renameOperation = renameFile; + + if (!renameOperation) { + resolve(); + } + + renameOperation() + .then(() => { + toast.success('Item renamed successfully'); + resolve(); + }) + .catch((err) => reject(err)); }); }; -export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getState) => { +export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); @@ -443,36 +473,41 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat const folderWithSameNameExists = find( parentFolder.items, - (i) => i.type === 'folder' && trim(i.name) === trim(newName) + (i) => i.type === 'folder' && trim(i?.filename) === trim(newFilename) ); if (folderWithSameNameExists) { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); } - const collectionPath = `${parentFolder.pathname}${PATH_SEPARATOR}${newName}`; + set(item, 'name', newName); + set(item, 'filename', newFilename); + set(item, 'root.meta.name', newName); + + const collectionPath = path.join(parentFolder.pathname, newFilename); ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject); return; } const parentItem = findParentItemInCollection(collectionCopy, itemUid); - const filename = resolveRequestFilename(newName); + const filename = resolveRequestFilename(newFilename); const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item)); - itemToSave.name = trim(newName); + set(itemToSave, 'name', trim(newName)); + set(itemToSave, 'filename', trim(filename)); if (!parentItem) { const reqWithSameNameExists = find( collection.items, (i) => i.type !== 'folder' && trim(i.filename) === trim(filename) ); if (!reqWithSameNameExists) { - const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`; + const fullPathname = path.join(collection.pathname, filename); const { ipcRenderer } = window; const requestItems = filter(collection.items, (i) => i.type !== 'folder'); itemToSave.seq = requestItems ? requestItems.length + 1 : 1; itemSchema .validate(itemToSave) - .then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave)) + .then(() => ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave)) .then(resolve) .catch(reject); @@ -481,7 +516,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat uid: uuid(), type: 'OPEN_REQUEST', collectionUid, - itemPathname: fullName + itemPathname: fullPathname }) ); } else { @@ -493,8 +528,8 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat (i) => i.type !== 'folder' && trim(i.filename) === trim(filename) ); if (!reqWithSameNameExists) { - const dirname = getDirectoryName(item.pathname); - const fullName = isWindowsPath(item.pathname) ? path.win32.join(dirname, filename) : path.join(dirname, filename); + const dirname = path.dirname(item.pathname); + const fullName = path.join(dirname, filename); const { ipcRenderer } = window; const requestItems = filter(parentItem.items, (i) => i.type !== 'folder'); itemToSave.seq = requestItems ? requestItems.length + 1 : 1; @@ -719,7 +754,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di }; export const newHttpRequest = (params) => (dispatch, getState) => { - const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params; + const { requestName, filename, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params; return new Promise((resolve, reject) => { const state = getState(); @@ -747,6 +782,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => { uid: uuid(), type: requestType, name: requestName, + filename, request: { method: requestMethod, url: requestUrl, @@ -769,46 +805,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => { }; // itemUid is null when we are creating a new request at the root level - const filename = resolveRequestFilename(requestName); + const resolvedFilename = resolveRequestFilename(filename); if (!itemUid) { const reqWithSameNameExists = find( collection.items, - (i) => i.type !== 'folder' && trim(i.filename) === trim(filename) + (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename) ); const requestItems = filter(collection.items, (i) => i.type !== 'folder'); item.seq = requestItems.length + 1; if (!reqWithSameNameExists) { - const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`; + const fullName = path.join(collection.pathname, resolvedFilename); const { ipcRenderer } = window; - ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject); - // task middleware will track this and open the new request in a new tab once request is created - dispatch( - insertTaskIntoQueue({ - uid: uuid(), - type: 'OPEN_REQUEST', - collectionUid, - itemPathname: fullName - }) - ); - } else { - return reject(new Error('Duplicate request names are not allowed under the same folder')); - } - } else { - const currentItem = findItemInCollection(collection, itemUid); - if (currentItem) { - const reqWithSameNameExists = find( - currentItem.items, - (i) => i.type !== 'folder' && trim(i.filename) === trim(filename) - ); - const requestItems = filter(currentItem.items, (i) => i.type !== 'folder'); - item.seq = requestItems.length + 1; - if (!reqWithSameNameExists) { - const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${filename}`; - const { ipcRenderer } = window; - - ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject); + ipcRenderer.invoke('renderer:new-request', fullName, item).then(() => { // task middleware will track this and open the new request in a new tab once request is created dispatch( insertTaskIntoQueue({ @@ -818,6 +828,35 @@ export const newHttpRequest = (params) => (dispatch, getState) => { itemPathname: fullName }) ); + resolve(); + }).catch(reject); + } else { + return reject(new Error('Duplicate request names are not allowed under the same folder')); + } + } else { + const currentItem = findItemInCollection(collection, itemUid); + if (currentItem) { + const reqWithSameNameExists = find( + currentItem.items, + (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename) + ); + const requestItems = filter(currentItem.items, (i) => i.type !== 'folder'); + item.seq = requestItems.length + 1; + if (!reqWithSameNameExists) { + const fullName = path.join(currentItem.pathname, resolvedFilename); + const { ipcRenderer } = window; + ipcRenderer.invoke('renderer:new-request', fullName, item).then(() => { + // task middleware will track this and open the new request in a new tab once request is created + dispatch( + insertTaskIntoQueue({ + uid: uuid(), + type: 'OPEN_REQUEST', + collectionUid, + itemPathname: fullName + }) + ); + resolve(); + }).catch(reject); } else { return reject(new Error('Duplicate request names are not allowed under the same folder')); } @@ -859,16 +898,18 @@ export const importEnvironment = (name, variables, collectionUid) => (dispatch, if (!collection) { return reject(new Error('Collection not found')); } + + const sanitizedName = sanitizeName(name); ipcRenderer - .invoke('renderer:create-environment', collection.pathname, name, variables) + .invoke('renderer:create-environment', collection.pathname, sanitizedName, variables) .then( dispatch( updateLastAction({ collectionUid, lastAction: { type: 'ADD_ENVIRONMENT', - payload: name + payload: sanitizedName } }) ) @@ -891,15 +932,17 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g return reject(new Error('Environment not found')); } + const sanitizedName = sanitizeName(name); + ipcRenderer - .invoke('renderer:create-environment', collection.pathname, name, baseEnv.variables) + .invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables) .then( dispatch( updateLastAction({ collectionUid, lastAction: { type: 'ADD_ENVIRONMENT', - payload: name + payload: sanitizedName } }) ) @@ -923,12 +966,13 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di return reject(new Error('Environment not found')); } + const sanitizedName = sanitizeName(newName); const oldName = environment.name; - environment.name = newName; + environment.name = sanitizedName; environmentSchema .validate(environment) - .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, newName)) + .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName)) .then(resolve) .catch(reject); }); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 43fc1c946..010ca49b6 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -16,10 +16,10 @@ import { isItemARequest } from 'utils/collections'; import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url'; -import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform'; +import { getSubdirectoriesFromRoot } from 'utils/common/platform'; import toast from 'react-hot-toast'; import mime from 'mime-types'; -import path from 'node:path'; +import path from 'utils/common/path'; const initialState = { collections: [], @@ -1655,25 +1655,29 @@ export const collectionsSlice = createSlice({ } if (isFolderRoot) { - const folderPath = getDirectoryName(file.meta.pathname); + const folderPath = path.dirname(file.meta.pathname); const folderItem = findItemInCollectionByPathname(collection, folderPath); if (folderItem) { + if (file?.data?.meta?.name) { + folderItem.name = file?.data?.meta?.name; + } folderItem.root = file.data; } return; } if (collection) { - const dirname = getDirectoryName(file.meta.pathname); + const dirname = path.dirname(file.meta.pathname); const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname); let currentPath = collection.pathname; let currentSubItems = collection.items; for (const directoryName of subDirectories) { - let childItem = currentSubItems.find((f) => f.type === 'folder' && f.name === directoryName); + let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName); + currentPath = path.join(currentPath, directoryName); if (!childItem) { childItem = { uid: uuid(), - pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`, + pathname: currentPath, name: directoryName, collapsed: true, type: 'folder', @@ -1681,8 +1685,6 @@ export const collectionsSlice = createSlice({ }; currentSubItems.push(childItem); } - - currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`; currentSubItems = childItem.items; } @@ -1732,20 +1734,20 @@ export const collectionsSlice = createSlice({ let currentPath = collection.pathname; let currentSubItems = collection.items; for (const directoryName of subDirectories) { - let childItem = currentSubItems.find((f) => f.type === 'folder' && f.name === directoryName); + let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName); + currentPath = path.join(currentPath, directoryName); if (!childItem) { childItem = { uid: uuid(), - pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`, - name: directoryName, + pathname: currentPath, + name: dir?.meta?.name || directoryName, + filename: directoryName, collapsed: true, type: 'folder', items: [] }; currentSubItems.push(childItem); } - - currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`; currentSubItems = childItem.items; } addDepth(collection.items); @@ -1753,11 +1755,25 @@ export const collectionsSlice = createSlice({ }, collectionChangeFileEvent: (state, action) => { const { file } = action.payload; + const isCollectionRoot = file.meta.collectionRoot ? true : false; + const isFolderRoot = file.meta.folderRoot ? true : false; const collection = findCollectionByUid(state.collections, file.meta.collectionUid); + if (isCollectionRoot) { + if (collection) { + collection.root = file.data; + } + return; + } - // check and update collection root - if (collection && file.meta.collectionRoot) { - collection.root = file.data; + if (isFolderRoot) { + const folderPath = path.dirname(file.meta.pathname); + const folderItem = findItemInCollectionByPathname(collection, folderPath); + if (folderItem) { + if (file?.data?.meta?.name) { + folderItem.name = file?.data?.meta?.name; + } + folderItem.root = file.data; + } return; } diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 3ac612c62..04d787f80 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -1,7 +1,6 @@ import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash'; import { uuid } from 'utils/common'; -import path from 'path'; -import slash from 'utils/common/slash'; +import path from 'utils/common/path'; const replaceTabsWithSpaces = (str, numSpaces = 2) => { if (!str || !str.length || !isString(str)) { @@ -90,7 +89,7 @@ export const findCollectionByItemUid = (collections, itemUid) => { }; export const findItemByPathname = (items = [], pathname) => { - return find(items, (i) => slash(i.pathname) === slash(pathname)); + return find(items, (i) => i.pathname === pathname); }; export const findItemInCollectionByPathname = (collection, pathname) => { @@ -307,6 +306,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} uid: si.uid, type: si.type, name: si.name, + filename: si.filename, seq: si.seq }; diff --git a/packages/bruno-app/src/utils/common/path.js b/packages/bruno-app/src/utils/common/path.js new file mode 100644 index 000000000..f85a15d3c --- /dev/null +++ b/packages/bruno-app/src/utils/common/path.js @@ -0,0 +1,12 @@ +import platform from 'platform'; +import path from 'path'; + +const isWindowsOS = () => { + const os = platform.os; + const osFamily = os.family.toLowerCase(); + return osFamily.includes('windows'); +}; + +const brunoPath = isWindowsOS() ? path.win32 : path.posix; + +export default brunoPath; diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js index c50ded79a..dc1d7d984 100644 --- a/packages/bruno-app/src/utils/common/platform.js +++ b/packages/bruno-app/src/utils/common/platform.js @@ -1,7 +1,6 @@ import trim from 'lodash/trim'; -import path from 'path'; -import slash from './slash'; import platform from 'platform'; +import path from './path'; export const isElectron = () => { if (!window) { @@ -16,35 +15,11 @@ export const resolveRequestFilename = (name) => { }; export const getSubdirectoriesFromRoot = (rootPath, pathname) => { - // convert to unix style path - pathname = slash(pathname); - rootPath = slash(rootPath); const relativePath = path.relative(rootPath, pathname); return relativePath ? relativePath.split(path.sep) : []; }; - -export const isWindowsPath = (pathname) => { - - if (!isWindowsOS()) { - return false; - } - - // Check for Windows drive letter format (e.g., "C:\") - const hasDriveLetter = /^[a-zA-Z]:\\/.test(pathname); - - // Check for UNC path format (e.g., "\\server\share") a.k.a. network path || WSL path - const isUNCPath = pathname.startsWith('\\\\'); - - return hasDriveLetter || isUNCPath; -}; - - -export const getDirectoryName = (pathname) => { - return isWindowsPath(pathname) ? path.win32.dirname(pathname) : path.dirname(pathname); -}; - export const isWindowsOS = () => { const os = platform.os; const osFamily = os.family.toLowerCase(); @@ -59,8 +34,6 @@ export const isMacOS = () => { return osFamily.includes('os x'); }; -export const PATH_SEPARATOR = isWindowsOS() ? '\\' : '/'; - export const getAppInstallDate = () => { let dateString = localStorage.getItem('bruno.installedOn'); diff --git a/packages/bruno-app/src/utils/common/regex.js b/packages/bruno-app/src/utils/common/regex.js index 53f46741e..9338288f0 100644 --- a/packages/bruno-app/src/utils/common/regex.js +++ b/packages/bruno-app/src/utils/common/regex.js @@ -1 +1,55 @@ +const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid characters with hyphens +const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i; +const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start +const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters +const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed + export const variableNameRegex = /^[\w-.]*$/; + +export const sanitizeName = (name) => { + name = name + .replace(invalidCharacters, '-') // replace invalid characters with hyphens + .replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces + .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens) + return name; +}; + +export const validateName = (name) => { + if (!name) return false; + if (name.length > 255) return false; // max name length + + if (reservedDeviceNames.test(name)) return false; // windows reserved names + + return ( + firstCharacter.test(name) && + middleCharacters.test(name) && + lastCharacter.test(name) + ); +}; + +export const validateNameError = (name) => { + if (!name) return "Name cannot be empty."; + if (name.length > 255) { + return "Name cannot exceed 255 characters."; + } + + if (reservedDeviceNames.test(name)) { + return "Name cannot be a reserved device name."; + } + + if (!firstCharacter.test(name[0])) { + return "Invalid first character."; + } + + for (let i = 1; i < name.length - 1; i++) { + if (!middleCharacters.test(name[i])) { + return `Invalid character '${name[i]}' at position ${i + 1}.`; + } + } + + if (!lastCharacter.test(name[name.length - 1])) { + return "Invalid last character."; + } + + return ''; +}; \ No newline at end of file diff --git a/packages/bruno-app/src/utils/common/regex.spec.js b/packages/bruno-app/src/utils/common/regex.spec.js new file mode 100644 index 000000000..e7a8b8d36 --- /dev/null +++ b/packages/bruno-app/src/utils/common/regex.spec.js @@ -0,0 +1,166 @@ +const { describe, it, expect } = require('@jest/globals'); + +import { sanitizeName, validateName } from './regex'; + +describe('regex validators', () => { + describe('sanitize name', () => { + it('should remove invalid characters', () => { + expect(sanitizeName('hello world')).toBe('hello world'); + expect(sanitizeName('hello-world')).toBe('hello-world'); + expect(sanitizeName('hello_world')).toBe('hello_world'); + expect(sanitizeName('hello_world-')).toBe('hello_world-'); + expect(sanitizeName('hello_world-123')).toBe('hello_world-123'); + expect(sanitizeName('hello_world-123!@#$%^&*()')).toBe('hello_world-123!@#$%^&-()'); + expect(sanitizeName('hello_world?')).toBe('hello_world-'); + expect(sanitizeName('foo/bar/')).toBe('foo-bar-'); + expect(sanitizeName('foo\\bar\\')).toBe('foo-bar-'); + }); + + it('should remove leading hyphens', () => { + expect(sanitizeName('-foo')).toBe('foo'); + expect(sanitizeName('---foo')).toBe('foo'); + expect(sanitizeName('-foo-bar')).toBe('foo-bar'); + }); + + it('should remove trailing periods', () => { + expect(sanitizeName('.file')).toBe('file'); + expect(sanitizeName('.file.')).toBe('file'); + expect(sanitizeName('file.')).toBe('file'); + expect(sanitizeName('file.name.')).toBe('file.name'); + expect(sanitizeName('hello world.')).toBe('hello world'); + }); + + it('should handle filenames with only invalid characters', () => { + expect(sanitizeName('<>:"/\\|?*')).toBe(''); + expect(sanitizeName('::::')).toBe(''); + }); + + it('should handle filenames with a mix of valid and invalid characters', () => { + expect(sanitizeName('test<>:"/\\|?*')).toBe('test---------'); + expect(sanitizeName('foo')).toBe('foo-bar-'); + }); + + it('should remove control characters', () => { + expect(sanitizeName('foo\x00bar')).toBe('foo-bar'); + expect(sanitizeName('file\x1Fname')).toBe('file-name'); + }); + + it('should return an empty string if the name is empty or consists only of invalid characters', () => { + expect(sanitizeName('')).toBe(''); + expect(sanitizeName('<>:"/\\|?*')).toBe(''); + }); + + it('should handle filenames with multiple consecutive invalid characters', () => { + expect(sanitizeName('foo< { + expect(sanitizeName(' ')).toBe(''); + }); + + it('should handle names with leading/trailing spaces', () => { + expect(sanitizeName(' foo bar ')).toBe('foo bar'); + }); + + it('should preserve valid non-ASCII characters', () => { + expect(sanitizeName('brunó')).toBe('brunó'); + expect(sanitizeName('文件')).toBe('文件'); + expect(sanitizeName('brunfais')).toBe('brunfais'); + expect(sanitizeName('brunai')).toBe('brunai'); + expect(sanitizeName('brunsборка')).toBe('brunsборка'); + expect(sanitizeName('brunпривет')).toBe('brunпривет'); + expect(sanitizeName('🐶')).toBe('🐶'); + expect(sanitizeName('brunfais🐶')).toBe('brunfais🐶'); + expect(sanitizeName('file-🐶-bruno')).toBe('file-🐶-bruno'); + expect(sanitizeName('helló')).toBe('helló'); + }); + + it('should preserve case sensitivity', () => { + expect(sanitizeName('FileName')).toBe('FileName'); + expect(sanitizeName('fileNAME')).toBe('fileNAME'); + }); + + it('should handle filenames with multiple consecutive periods (only remove trailing)', () => { + expect(sanitizeName('file.name...')).toBe('file.name'); + expect(sanitizeName('...file')).toBe('file'); + expect(sanitizeName('file.name... ')).toBe('file.name'); + expect(sanitizeName(' ...file')).toBe('file'); + expect(sanitizeName(' ...file ')).toBe('file'); + expect(sanitizeName(' ...file.... ')).toBe('file'); + }); + + it('should handle very long filenames', () => { + const longName = 'a'.repeat(250) + '.txt'; + expect(sanitizeName(longName)).toBe(longName); + }); + + it('should handle names with leading/trailing invalid characters', () => { + expect(sanitizeName('-foo/bar-')).toBe('foo-bar-'); + expect(sanitizeName('/foo\\bar/')).toBe('foo-bar-'); + }); + + it('should handle different language unicode characters', () => { + expect(sanitizeName('你好世界!?@#$%^&*()')).toBe('你好世界!-@#$%^&-()'); + expect(sanitizeName('こんにちは世界!?@#$%^&*()')).toBe('こんにちは世界!-@#$%^&-()'); + expect(sanitizeName('안녕하세요 세계!?@#$%^&*()')).toBe('안녕하세요 세계!-@#$%^&-()'); + expect(sanitizeName('مرحبا بالعالم!?@#$%^&*()')).toBe('مرحبا بالعالم!-@#$%^&-()'); + expect(sanitizeName('Здравствуй мир!?@#$%^&*()')).toBe('Здравствуй мир!-@#$%^&-()'); + expect(sanitizeName('नमस्ते दुनिया!?@#$%^&*()')).toBe('नमस्ते दुनिया!-@#$%^&-()'); + expect(sanitizeName('สวัสดีชาวโลก!?@#$%^&*()')).toBe('สวัสดีชาวโลก!-@#$%^&-()'); + expect(sanitizeName('γειά σου κόσμος!?@#$%^&*()')).toBe('γειά σου κόσμος!-@#$%^&-()'); + }); + + }); +}); + +describe('sanitizeName and validateName', () => { + it('should sanitize and then validate valid names', () => { + const validNames = [ + 'valid_filename.txt', + ' valid name ', + ' valid-name ', + 'valid<>name.txt', + 'file/with?invalid*chars' + ]; + + validNames.forEach(name => { + const sanitized = sanitizeName(name); + expect(validateName(sanitized)).toBe(true); + }); + }); + + it('should sanitize and then validate names with reserved device names', () => { + const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT2']; + + reservedNames.forEach(name => { + const sanitized = sanitizeName(name); + expect(validateName(sanitized)).toBe(false); + }); + }); + + it('should sanitize invalid names to empty strings', () => { + const invalidNames = [ + ' <>:"/\\|?* ', + ' ... ', + ' ', + ]; + + invalidNames.forEach(name => { + const sanitized = sanitizeName(name); + expect(validateName(sanitized)).toBe(false); + }); + }); + + it('should return false for reserved device names with leading/trailing spaces', () => { + const mixedNames = [ + 'AUX ', + ' COM1 ' + ]; + + mixedNames.forEach(name => { + const sanitized = sanitizeName(name); + expect(validateName(sanitized)).toBe(false); + }); + }); +}); diff --git a/packages/bruno-app/src/utils/common/slash.js b/packages/bruno-app/src/utils/common/slash.js deleted file mode 100644 index a2b39e94f..000000000 --- a/packages/bruno-app/src/utils/common/slash.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * MIT License - * - * Copyright (c) Sindre Sorhus (https://sindresorhus.com) - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -const slash = (path) => { - const isExtendedLengthPath = /^\\\\\?\\/.test(path); - - if (isExtendedLengthPath) { - return path; - } - - return path.replace(/\\/g, '/'); -}; - -export default slash; diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index 9d370a455..64db764fb 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -62,7 +62,6 @@ export const updateUidsInCollection = (_collection) => { export const transformItemsInCollection = (collection) => { const transformItems = (items = []) => { each(items, (item) => { - item.name = normalizeFileName(item.name); if (['http', 'graphql'].includes(item.type)) { item.type = `${item.type}-request`; diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index 7bd74c43b..a6b7a178c 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -45,9 +45,8 @@ const openCollectionDialog = async (win, watcher) => { const { filePaths } = await dialog.showOpenDialog(win, { properties: ['openDirectory', 'createDirectory'] }); - if (filePaths && filePaths[0]) { - const resolvedPath = normalizeAndResolvePath(filePaths[0]); + const resolvedPath = path.resolve(filePaths[0]); if (isDirectory(resolvedPath)) { openCollection(win, watcher, resolvedPath); } else { diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index b2b60fd55..39c22bb1a 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -2,8 +2,8 @@ const _ = require('lodash'); const fs = require('fs'); const path = require('path'); const chokidar = require('chokidar'); -const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath, sizeInMB } = require('../utils/filesystem'); -const { bruToEnvJson, bruToJson, bruToJsonViaWorker ,collectionBruToJson } = require('../bru'); +const { hasBruExtension, isWSLPath, normalizeAndResolvePath, sizeInMB } = require('../utils/filesystem'); +const { bruToEnvJson, bruToJson, bruToJsonViaWorker, collectionBruToJson } = require('../bru'); const { dotenvToJson } = require('@usebruno/lang'); const { uuid } = require('../utils/common'); @@ -319,20 +319,24 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread } }; -const addDirectory = (win, pathname, collectionUid, collectionPath) => { +const addDirectory = async (win, pathname, collectionUid, collectionPath) => { const envDirectory = path.join(collectionPath, 'environments'); if (pathname === envDirectory) { return; } + let name = path.basename(pathname); + const directory = { meta: { collectionUid, pathname, - name: path.basename(pathname) + name } }; + + win.webContents.send('main:collection-tree-updated', 'addDir', directory); }; @@ -399,6 +403,30 @@ const change = async (win, pathname, collectionUid, collectionPath) => { } } + if (path.basename(pathname) === 'folder.bru') { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname), + folderRoot: true + } + }; + + try { + let bruContent = fs.readFileSync(pathname, 'utf8'); + + file.data = await collectionBruToJson(bruContent); + + hydrateBruCollectionFileWithUuid(file.data); + win.webContents.send('main:collection-tree-updated', 'change', file); + return; + } catch (err) { + console.error(err); + return; + } + } + if (hasBruExtension(pathname)) { try { const file = { @@ -439,18 +467,29 @@ const unlink = (win, pathname, collectionUid, collectionPath) => { } }; -const unlinkDir = (win, pathname, collectionUid, collectionPath) => { +const unlinkDir = async (win, pathname, collectionUid, collectionPath) => { const envDirectory = path.join(collectionPath, 'environments'); if (pathname === envDirectory) { return; } + + const folderBruFilePath = path.join(pathname, `folder.bru`); + + let name = path.basename(pathname); + + if (fs.existsSync(folderBruFilePath)) { + let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); + let folderBruData = await collectionBruToJson(folderBruFileContent); + name = folderBruData?.meta?.name || name; + } + const directory = { meta: { collectionUid, pathname, - name: path.basename(pathname) + name } }; win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory); @@ -477,14 +516,13 @@ class Watcher { setTimeout(() => { const watcher = chokidar.watch(watchPath, { ignoreInitial: false, - usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false, + usePolling: isWSLPath(watchPath) || forcePolling ? true : false, ignored: (filepath) => { - const normalizedPath = isWSLPath(filepath) ? normalizeWslPath(filepath) : normalizeAndResolvePath(filepath); + const normalizedPath = normalizeAndResolvePath(filepath); const relativePath = path.relative(watchPath, normalizedPath); return ignores.some((ignorePattern) => { - const normalizedIgnorePattern = isWSLPath(ignorePattern) ? normalizeWslPath(ignorePattern) : ignorePattern.replace(/\\/g, '/'); - return relativePath === normalizedIgnorePattern || relativePath.startsWith(normalizedIgnorePattern); + return relativePath === ignorePattern || relativePath.startsWith(ignorePattern); }); }, persistent: true, diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 6fcb3723e..d19171e65 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -4,10 +4,9 @@ const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); const { ipcMain, shell, dialog, app } = require('electron'); -const { envJsonToBru, bruToJson, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); +const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); const { - isValidPathname, writeFile, hasBruExtension, isDirectory, @@ -15,16 +14,15 @@ const { browseFiles, createDirectory, searchForBruFiles, - sanitizeDirectoryName, + sanitizeName, isWSLPath, - normalizeWslPath, - normalizeAndResolvePath, safeToRename, isWindowsOS, - isValidFilename, + validateName, hasSubDirectories, getCollectionStats, - sizeInMB + sizeInMB, + safeWriteFileSync } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -74,7 +72,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection 'renderer:create-collection', async (event, collectionName, collectionFolderName, collectionLocation) => { try { - collectionFolderName = sanitizeDirectoryName(collectionFolderName); + collectionFolderName = sanitizeName(collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName); if (fs.existsSync(dirPath)) { const files = fs.readdirSync(dirPath); @@ -83,7 +81,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`collection: ${dirPath} already exists and is not empty`); } } - if (!isValidPathname(path.basename(dirPath))) { + + if (!validateName(path.basename(dirPath))) { throw new Error(`collection: invalid pathname - ${dirPath}`); } @@ -116,13 +115,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle( 'renderer:clone-collection', async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => { - collectionFolderName = sanitizeDirectoryName(collectionFolderName); + collectionFolderName = sanitizeName(collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName); if (fs.existsSync(dirPath)) { throw new Error(`collection: ${dirPath} already exists`); } - if (!isValidPathname(path.basename(dirPath))) { + if (!validateName(path.basename(dirPath))) { throw new Error(`collection: invalid pathname - ${dirPath}`); } @@ -221,8 +220,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (fs.existsSync(pathname)) { throw new Error(`path: ${pathname} already exists`); } - if (!isValidFilename(request.name)) { - throw new Error(`path: ${request.name}.bru is not a valid filename`); + // For the actual filename part, we want to be strict + if (!validateName(request?.filename)) { + throw new Error(`${request.filename}.bru is not a valid filename`); } const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); @@ -358,18 +358,53 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // rename item - ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { - const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); - // const parentDir = path.dirname(oldPath); - const isWindowsOSAndNotWSLAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath); - // let parentDirUnwatched = false; - // let parentDirRewatched = false; - + ipcMain.handle('renderer:rename-item-name', async (event, { itemPath, newName }) => { try { - // Normalize paths if they are WSL paths - oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath); - newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath); + if (!fs.existsSync(itemPath)) { + throw new Error(`path: ${itemPath} does not exist`); + } + + if (isDirectory(itemPath)) { + const folderBruFilePath = path.join(itemPath, 'folder.bru'); + let folderBruFileJsonContent; + if (fs.existsSync(folderBruFilePath)) { + const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); + folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + } else { + folderBruFileJsonContent = {}; + } + + folderBruFileJsonContent.meta = { + name: newName, + }; + + const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); + await writeFile(folderBruFilePath, folderBruFileContent); + + return; + } + + const isBru = hasBruExtension(itemPath); + if (!isBru) { + throw new Error(`path: ${itemPath} is not a bru file`); + } + + const data = fs.readFileSync(itemPath, 'utf8'); + const jsonData = await bruToJson(data); + jsonData.name = newName; + const content = await jsonToBru(jsonData); + await writeFile(itemPath, content); + } catch (error) { + return Promise.reject(error); + } + }); + + // rename item + ipcMain.handle('renderer:rename-item-filename', async (event, { oldPath, newPath, newName, newFilename }) => { + const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); + const isWindowsOSAndNotWSLPathAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath); + try { // Check if the old path exists if (!fs.existsSync(oldPath)) { throw new Error(`path: ${oldPath} does not exist`); @@ -380,6 +415,22 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } if (isDirectory(oldPath)) { + const folderBruFilePath = path.join(oldPath, 'folder.bru'); + let folderBruFileJsonContent; + if (fs.existsSync(folderBruFilePath)) { + const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); + folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + } else { + folderBruFileJsonContent = {}; + } + + folderBruFileJsonContent.meta = { + name: newName, + }; + + const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); + await writeFile(folderBruFilePath, folderBruFileContent); + const bruFilesAtSource = await searchForBruFiles(oldPath); for (let bruFile of bruFilesAtSource) { @@ -387,19 +438,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection moveRequestUid(bruFile, newBruFilePath); } - // watcher.unlinkItemPathInWatcher(parentDir); - // parentDirUnwatched = true; - /** * If it is windows OS - * And it is not WSL path (meaning it's not linux running on windows using WSL) + * And it is not a WSL path (meaning it is not running in WSL (linux pathtype)) * And it has sub directories * Only then we need to use the temp dir approach to rename the folder * * Windows OS would sometimes throw error when renaming a folder with sub directories * This is an alternative approach to avoid that error */ - if (isWindowsOSAndNotWSLAndItemHasSubDirectories) { + if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) { await fsExtra.copy(oldPath, tempDir); await fsExtra.remove(oldPath); await fsExtra.move(tempDir, newPath, { overwrite: true }); @@ -407,8 +455,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } else { await fs.renameSync(oldPath, newPath); } - // watcher.addItemPathInWatcher(parentDir); - // parentDirRewatched = true; return newPath; } @@ -417,8 +463,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`path: ${oldPath} is not a bru file`); } - if (!isValidFilename(newName)) { - throw new Error(`path: ${newName} is not a valid filename`); + if (!validateName(newFilename)) { + throw new Error(`path: ${newFilename} is not a valid filename`); } // update name in file and save new copy, then delete old copy @@ -433,15 +479,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection return newPath; } catch (error) { - // in case an error occurs during the rename file operations after unlinking the parent dir - // and the rewatch fails, we need to add it back to watcher - // if (parentDirUnwatched && !parentDirRewatched) { - // watcher.addItemPathInWatcher(parentDir); - // } - // in case the rename file operations fails, and we see that the temp dir exists // and the old path does not exist, we need to restore the data from the temp dir to the old path - if (isWindowsOSAndNotWSLAndItemHasSubDirectories) { + if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) { if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) { try { await fsExtra.copy(tempDir, oldPath); @@ -457,12 +497,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // new folder - ipcMain.handle('renderer:new-folder', async (event, pathname) => { - const resolvedFolderName = sanitizeDirectoryName(path.basename(pathname)); + ipcMain.handle('renderer:new-folder', async (event, pathname, folderName) => { + const resolvedFolderName = sanitizeName(path.basename(pathname)); pathname = path.join(path.dirname(pathname), resolvedFolderName); try { if (!fs.existsSync(pathname)) { fs.mkdirSync(pathname); + const folderBruFilePath = path.join(pathname, 'folder.bru'); + let data = { + meta: { + name: folderName, + } + }; + const content = await jsonToCollectionBru(data, true); // isFolder flag + await writeFile(folderBruFilePath, content); } else { return Promise.reject(new Error('The directory already exists')); } @@ -522,7 +570,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => { try { - let collectionName = sanitizeDirectoryName(collection.name); + let collectionName = sanitizeName(collection.name); let collectionPath = path.join(collectionLocation, collectionName); if (fs.existsSync(collectionPath)) { @@ -533,13 +581,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const parseCollectionItems = (items = [], currentPath) => { items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { + let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`); const content = await jsonToBruViaWorker(item); - const filePath = path.join(currentPath, `${item.name}.bru`); - fs.writeFileSync(filePath, content); + const filePath = path.join(currentPath, sanitizedFilename); + safeWriteFileSync(filePath, content); } if (item.type === 'folder') { - item.name = sanitizeDirectoryName(item.name); - const folderPath = path.join(currentPath, item.name); + let sanitizedFolderName = sanitizeName(item?.filename || item?.name); + const folderPath = path.join(currentPath, sanitizedFolderName); fs.mkdirSync(folderPath); if (item?.root?.meta?.name) { @@ -548,7 +597,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection item.root, true // isFolder ); - fs.writeFileSync(folderBruFilePath, folderContent); + safeWriteFileSync(folderBruFilePath, folderContent); } if (item.items && item.items.length) { @@ -557,8 +606,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } // Handle items of type 'js' if (item.type === 'js') { - const filePath = path.join(currentPath, `${item.name}.js`); - fs.writeFileSync(filePath, item.fileContent); + let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.js`); + const filePath = path.join(currentPath, sanitizedFilename); + safeWriteFileSync(filePath, item.fileContent); } }); }; @@ -571,8 +621,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection environments.forEach(async (env) => { const content = await envJsonToBru(env); - const filePath = path.join(envDirPath, `${env.name}.bru`); - fs.writeFileSync(filePath, content); + let sanitizedEnvFilename = sanitizeName(`${env.name}.bru`); + const filePath = path.join(envDirPath, sanitizedEnvFilename); + safeWriteFileSync(filePath, content); }); }; @@ -630,20 +681,21 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const parseCollectionItems = (items = [], currentPath) => { items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { - const content = await jsonToBruViaWorker(item); - const filePath = path.join(currentPath, `${item.name}.bru`); - fs.writeFileSync(filePath, content); + const content = await jsonToBruViaWorker(item); + const filePath = path.join(currentPath, item.filename); + safeWriteFileSync(filePath, content); } if (item.type === 'folder') { - const folderPath = path.join(currentPath, item.name); + const folderPath = path.join(currentPath, item.filename); fs.mkdirSync(folderPath); // If folder has a root element, then I should write its folder.bru file if (item.root) { const folderContent = await jsonToCollectionBru(item.root, true); + folderContent.name = item.name; if (folderContent) { const bruFolderPath = path.join(folderPath, `folder.bru`); - fs.writeFileSync(bruFolderPath, folderContent); + safeWriteFileSync(bruFolderPath, folderContent); } } @@ -661,7 +713,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const folderContent = await jsonToCollectionBru(itemFolder.root, true); if (folderContent) { const bruFolderPath = path.join(collectionPath, `folder.bru`); - fs.writeFileSync(bruFolderPath, folderContent); + safeWriteFileSync(bruFolderPath, folderContent); } } @@ -697,7 +749,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection moveRequestUid(itemPath, newItemPath); fs.unlinkSync(itemPath); - fs.writeFileSync(newItemPath, itemContent); + safeWriteFileSync(newItemPath, itemContent); } catch (error) { return Promise.reject(error); } diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index bb8d17e97..f96cec877 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -254,16 +254,8 @@ const hydrateRequestWithUuid = (request, pathname) => { return request; }; -const slash = (path) => { - const isExtendedLengthPath = /^\\\\\?\\/.test(path); - if (isExtendedLengthPath) { - return path; - } - return path?.replace?.(/\\/g, '/'); -}; - const findItemByPathname = (items = [], pathname) => { - return find(items, (i) => slash(i.pathname) === slash(pathname)); + return find(items, (i) => i.pathname === pathname); }; const findItemInCollectionByPathname = (collection, pathname) => { @@ -280,7 +272,6 @@ module.exports = { flattenItems, findItem, findItemInCollection, - slash, findItemByPathname, findItemInCollectionByPathname, findParentItemInCollection, diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index a9c8597e7..aaff867b1 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -44,6 +44,11 @@ const hasSubDirectories = (dir) => { }; const normalizeAndResolvePath = (pathname) => { + + if (isWSLPath(pathname)) { + return normalizeWSLPath(pathname); + } + if (isSymbolicLink(pathname)) { const absPath = path.dirname(pathname); const targetPath = path.resolve(absPath, fs.readlinkSync(pathname)); @@ -59,18 +64,20 @@ const normalizeAndResolvePath = (pathname) => { function isWSLPath(pathname) { // Check if the path starts with the WSL prefix // eg. "\\wsl.localhost\Ubuntu\home\user\bruno\collection\scripting\api\req\getHeaders.bru" - return pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost\\'); + return pathname.startsWith('\\\\') || pathname.startsWith('//') || pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost'); + } -function normalizeWslPath(pathname) { +function normalizeWSLPath(pathname) { // Replace the WSL path prefix and convert forward slashes to backslashes // This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion) return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\'); } + const writeFile = async (pathname, content, isBinary = false) => { try { - await fs.writeFile(pathname, content, { + await safeWriteFile(pathname, content, { encoding: !isBinary ? "utf-8" : null }); } catch (err) { @@ -110,7 +117,7 @@ const browseDirectory = async (win) => { return false; } - const resolvedPath = normalizeAndResolvePath(filePaths[0]); + const resolvedPath = path.resolve(filePaths[0]); return isDirectory(resolvedPath) ? resolvedPath : false; }; @@ -124,7 +131,7 @@ const browseFiles = async (win, filters = [], properties = []) => { return []; } - return filePaths.map((path) => normalizeAndResolvePath(path)).filter((path) => isFile(path)); + return filePaths.map((path) => path.resolve(path)).filter((path) => isFile(path)); }; const chooseFileToSave = async (win, preferredFileName = '') => { @@ -154,28 +161,36 @@ const searchForBruFiles = (dir) => { return searchForFiles(dir, '.bru'); }; -const sanitizeDirectoryName = (name) => { - return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim(); +const sanitizeName = (name) => { + const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; + name = name + .replace(invalidCharacters, '-') // replace invalid characters with hyphens + .replace(/^[.\s]+/, '') // remove leading dots and and spaces + .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens) + return name; }; const isWindowsOS = () => { return os.platform() === 'win32'; } -const isValidFilename = (fileName) => { - const inValidChars = /[\\/:*?"<>|]/; +const validateName = (name) => { + const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i; + const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start + const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters + const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed + if (name.length > 255) return false; // max name length - if (!fileName || inValidChars.test(fileName)) { - return false; - } + if (reservedDeviceNames.test(name)) return false; // windows reserved names - if (fileName.endsWith(' ') || fileName.endsWith('.') || fileName.startsWith('.')) { - return false; - } - - return true; + return ( + firstCharacter.test(name) && + middleCharacters.test(name) && + lastCharacter.test(name) + ); }; + const safeToRename = (oldPath, newPath) => { try { // If the new path doesn't exist, it's safe to rename @@ -244,6 +259,29 @@ const sizeInMB = (size) => { return size / (1024 * 1024); } +const getSafePathToWrite = (filePath) => { + const MAX_FILENAME_LENGTH = 255; // Common limit on most filesystems + let dir = path.dirname(filePath); + let ext = path.extname(filePath); + let base = path.basename(filePath, ext); + if (base.length + ext.length > MAX_FILENAME_LENGTH) { + base = sanitizeName(base); + base = base.slice(0, MAX_FILENAME_LENGTH - ext.length); + } + let safePath = path.join(dir, base + ext); + return safePath; +} + +async function safeWriteFile(filePath, data, options) { + const safePath = getSafePathToWrite(filePath); + await fs.writeFile(safePath, data, options); +} + +function safeWriteFileSync(filePath, data) { + const safePath = getSafePathToWrite(filePath); + fs.writeFileSync(safePath, data); +} + module.exports = { isValidPathname, exists, @@ -252,7 +290,7 @@ module.exports = { isDirectory, normalizeAndResolvePath, isWSLPath, - normalizeWslPath, + normalizeWSLPath, writeFile, hasJsonExtension, hasBruExtension, @@ -262,11 +300,13 @@ module.exports = { chooseFileToSave, searchForFiles, searchForBruFiles, - sanitizeDirectoryName, + sanitizeName, isWindowsOS, safeToRename, - isValidFilename, + validateName, hasSubDirectories, getCollectionStats, - sizeInMB + sizeInMB, + safeWriteFile, + safeWriteFileSync }; diff --git a/packages/bruno-electron/src/utils/filesystem.test.js b/packages/bruno-electron/src/utils/filesystem.test.js index 62d7b502f..a6e2db53a 100644 --- a/packages/bruno-electron/src/utils/filesystem.test.js +++ b/packages/bruno-electron/src/utils/filesystem.test.js @@ -1,26 +1,84 @@ -const { sanitizeDirectoryName } = require('./filesystem.js'); +const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath } = require('./filesystem.js'); -describe('sanitizeDirectoryName', () => { +describe('sanitizeName', () => { it('should replace invalid characters with hyphens', () => { - const input = '<>:"/\\|?*\x00-\x1F'; - const expectedOutput = '---'; - expect(sanitizeDirectoryName(input)).toEqual(expectedOutput); + const input = '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F'; + const expectedOutput = '----------------------------------------'; + expect(sanitizeName(input)).toEqual(expectedOutput); }); it('should not modify valid directory names', () => { const input = 'my-directory'; - expect(sanitizeDirectoryName(input)).toEqual(input); + expect(sanitizeName(input)).toEqual(input); }); it('should replace multiple invalid characters with a single hyphen', () => { const input = 'my<>invalid?directory'; - const expectedOutput = 'my-invalid-directory'; - expect(sanitizeDirectoryName(input)).toEqual(expectedOutput); + const expectedOutput = 'my--invalid-directory'; + expect(sanitizeName(input)).toEqual(expectedOutput); }); it('should handle names with slashes', () => { const input = 'my/invalid/directory'; const expectedOutput = 'my-invalid-directory'; - expect(sanitizeDirectoryName(input)).toEqual(expectedOutput); + expect(sanitizeName(input)).toEqual(expectedOutput); + }); +}); + +describe('WSL Path Utilities', () => { + describe('isWSLPath', () => { + it('should identify WSL paths starting with double backslash', () => { + expect(isWSLPath('\\\\wsl.localhost\\Ubuntu\\home\\user')).toBe(true); + }); + + it('should identify WSL paths starting with double forward slash', () => { + expect(isWSLPath('//wsl.localhost/Ubuntu/home/user')).toBe(true); + }); + + it('should identify WSL paths starting with /wsl.localhost/', () => { + expect(isWSLPath('/wsl.localhost/Ubuntu/home/user')).toBe(true); + }); + + it('should identify WSL paths starting with \\wsl.localhost', () => { + expect(isWSLPath('\\wsl.localhost\\Ubuntu\\home\\user')).toBe(true); + }); + + it('should return false for non-WSL paths', () => { + expect(isWSLPath('C:\\Users\\user\\Documents')).toBe(false); + expect(isWSLPath('/home/user/documents')).toBe(false); + expect(isWSLPath('relative/path')).toBe(false); + }); + }); + + describe('normalizeWSLPath', () => { + it('should convert forward slash WSL paths to backslash format', () => { + const input = '/wsl.localhost/Ubuntu/home/user/file.txt'; + const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt'; + expect(normalizeWSLPath(input)).toBe(expected); + }); + + it('should handle paths already in backslash format', () => { + const input = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt'; + expect(normalizeWSLPath(input)).toBe(input); + }); + + it('should convert mixed slash formats to backslash format', () => { + const input = '/wsl.localhost\\Ubuntu/home\\user/file.txt'; + const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt'; + expect(normalizeWSLPath(input)).toBe(expected); + }); + }); + + describe('normalizeAndResolvePath with WSL paths', () => { + it('should normalize WSL paths', () => { + const input = '/wsl.localhost/Ubuntu/home/user/file.txt'; + const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt'; + expect(normalizeAndResolvePath(input)).toBe(expected); + }); + + it('should handle already normalized WSL paths', () => { + const input = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt'; + expect(normalizeAndResolvePath(input)).toBe(input); + }); }); }); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index 4ccd8ebfc..863d13d6d 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -189,7 +189,7 @@ const addBruShimToContext = (vm, bru) => { const promise = vm.newPromise(); bru.runRequest(vm.dump(args)) .then((response) => { - const { status, statusText, headers, data, dataBuffer, size } = response || {}; + const { status, headers, data, dataBuffer, size, statusText } = response || {}; promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm)); }) .catch((err) => {