fix: refine backrest SFTP UI (#1193)
Release Please / release-please (push) Has been cancelled
Release Preview / call-reusable-release (push) Has been cancelled
Test / test-nix (push) Has been cancelled
Test / test-win (push) Has been cancelled
Update Restic / update-restic-version (push) Has been cancelled

This commit is contained in:
Gareth
2026-04-12 15:33:10 -07:00
committed by GitHub
parent 8e9470c954
commit 88f0fbc7aa
3 changed files with 169 additions and 146 deletions
+8 -32
View File
@@ -280,53 +280,29 @@ func (s *BackrestHandler) SetupSftp(ctx context.Context, req *connect.Request[v1
if port == "" {
port = "22"
}
user := req.Msg.Username
password := req.Msg.Password // Optional
if runtime.GOOS == "windows" {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("automated SFTP setup is not supported on Windows"))
}
// 1. Host Key Verification/Addition
if err := sftputil.AddHostKey(host, port, env.SSHDir()); err != nil {
return connect.NewResponse(&v1.SetupSftpResponse{
Error: fmt.Sprintf("Failed to add host key: %v", err),
}), nil
}
// 2. Generate Key
// 1. Generate key pair (local, always succeeds)
_, pubBytes, keyPath, err := sftputil.GenerateKey(host, env.SSHDir())
if err != nil {
return nil, fmt.Errorf("failed to generate key: %w", err)
}
pubKeyStr := string(pubBytes)
// 3. Install if password provided
if password != nil {
if err := sftputil.InstallKey(host, port, user, *password, pubBytes); err != nil {
return connect.NewResponse(&v1.SetupSftpResponse{
Error: fmt.Sprintf("Failed to install key: %v", err),
}), nil
}
// Verify
privPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read generated private key for verification: %w", err)
}
if err := sftputil.VerifyConnection(host, port, user, privPEM); err != nil {
return connect.NewResponse(&v1.SetupSftpResponse{
Error: fmt.Sprintf("Key installed but verification failed: %v", err),
}), nil
}
// 2. Scan remote host key into known_hosts (network, non-fatal)
var hostKeyWarning string
if err := sftputil.AddHostKey(host, port, env.SSHDir()); err != nil {
zap.S().Warnf("SFTP host key scan failed for %s: %v", host, err)
hostKeyWarning = fmt.Sprintf("Could not scan host key (%v). Add the host key to known_hosts manually or ensure the host is reachable.", err)
}
return connect.NewResponse(&v1.SetupSftpResponse{
PublicKey: pubKeyStr,
PublicKey: string(pubBytes),
KeyPath: keyPath,
KnownHostsPath: filepath.Join(env.SSHDir(), "known_hosts"),
Error: hostKeyWarning,
}), nil
}
+67 -25
View File
@@ -63,6 +63,7 @@
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz",
"integrity": "sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ant-design/fast-color": "^3.0.0"
}
@@ -72,6 +73,7 @@
"resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.0.1.tgz",
"integrity": "sha512-Lw1Z4cUQxdMmTNir67gU0HCpTl5TtkKCJPZ6UBvCqzcOTl/QmMFB6qAEoj8qFl0CuZDX9qQYa3m9+rEKfaBSbA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.11.1",
"@emotion/hash": "^0.8.0",
@@ -91,6 +93,7 @@
"resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz",
"integrity": "sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ant-design/cssinjs": "^2.0.1",
"@babel/runtime": "^7.23.2",
@@ -105,25 +108,29 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@ant-design/cssinjs/node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@ant-design/cssinjs/node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@ant-design/fast-color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.0.tgz",
"integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8.x"
}
@@ -133,6 +140,7 @@
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz",
"integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ant-design/colors": "^8.0.0",
"@ant-design/icons-svg": "^4.4.0",
@@ -151,13 +159,15 @@
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@ant-design/react-slick": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz",
"integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"clsx": "^2.1.1",
@@ -275,7 +285,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -538,8 +547,7 @@
"version": "2.10.2",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz",
"integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==",
"license": "(Apache-2.0 AND BSD-3-Clause)",
"peer": true
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@chakra-ui/react": {
"version": "3.30.0",
@@ -566,7 +574,6 @@
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.1.tgz",
"integrity": "sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@bufbuild/protobuf": "^2.7.0"
}
@@ -598,7 +605,6 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -693,7 +699,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1352,7 +1357,6 @@
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -1702,7 +1706,6 @@
"integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
@@ -2404,6 +2407,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
"integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.24.4"
},
@@ -2416,6 +2420,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.9.0.tgz",
"integrity": "sha512-2jbthe1QZrMBgtCvNKkJFjZYC3uKl4N/aYm5SsMvO3T+F+qRT1CGsSM9bXnh1rLj7jDk/GK0natShWF/jinhWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/select": "~1.3.0",
"@rc-component/tree": "~1.1.0",
@@ -2432,6 +2437,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-1.0.1.tgz",
"integrity": "sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
@@ -2446,6 +2452,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.1.2.tgz",
"integrity": "sha512-ilBYk1dLLJHu5Q74dF28vwtKUYQ42ZXIIDmqTuVy4rD8JQVvkXOs+KixVNbweyuIEtJYJ7+t+9GVD9dPc6N02w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/motion": "^1.1.4",
@@ -2462,6 +2469,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.0.3.tgz",
"integrity": "sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ant-design/fast-color": "^3.0.0",
"@rc-component/util": "^1.3.0",
@@ -2477,6 +2485,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz",
"integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0"
},
@@ -2490,6 +2499,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.5.1.tgz",
"integrity": "sha512-by4Sf/a3azcb89WayWuwG19/Y312xtu8N81HoVQQtnsBDylfs+dog98fTAvLinnpeoWG52m/M7QLRW6fXR3l1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.1.3",
"@rc-component/portal": "^2.0.0",
@@ -2506,6 +2516,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.3.0.tgz",
"integrity": "sha512-rE+sdXEmv2W25VBQ9daGbnb4J4hBIEKmdbj0b3xpY+K7TUmLXDIlSnoXraIbFZdGyek9WxxGKK887uRnFgI+pQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.0.0",
@@ -2522,6 +2533,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz",
"integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.2.1",
@@ -2537,6 +2549,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.6.0.tgz",
"integrity": "sha512-A7vrN8kExtw4sW06mrsgCb1rowhvBFFvQU6Bk/NL0Fj6Wet/5GF0QnGCxBu/sG3JI9FEhsJWES0D44BW2d0hzg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/async-validator": "^5.0.3",
"@rc-component/util": "^1.5.0",
@@ -2555,6 +2568,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.5.3.tgz",
"integrity": "sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.0.0",
"@rc-component/portal": "^2.0.0",
@@ -2571,6 +2585,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz",
"integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
@@ -2585,6 +2600,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz",
"integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/mini-decimal": "^1.0.1",
"@rc-component/util": "^1.4.0",
@@ -2600,6 +2616,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz",
"integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/input": "~1.1.0",
"@rc-component/menu": "~1.2.0",
@@ -2618,6 +2635,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz",
"integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/overflow": "^1.0.0",
@@ -2635,6 +2653,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz",
"integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.0"
},
@@ -2647,6 +2666,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.1.6.tgz",
"integrity": "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.2.0",
"clsx": "^2.1.1"
@@ -2661,6 +2681,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz",
"integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.2.0"
},
@@ -2677,6 +2698,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz",
"integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/util": "^1.2.1",
@@ -2695,6 +2717,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz",
"integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.11.1",
"@rc-component/resize-observer": "^1.0.1",
@@ -2711,6 +2734,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz",
"integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
@@ -2725,6 +2749,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.9.0.tgz",
"integrity": "sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/overflow": "^1.0.0",
"@rc-component/resize-observer": "^1.0.0",
@@ -2763,6 +2788,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.1.0.tgz",
"integrity": "sha512-P25IXWkzvBbyEtrAHRfqSNRkILXgAjDfuk0s4daPfHHO0XzVk3D3KJY3Lh069xwuBGtsTZpg+mP4WBLYl9GNaA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
@@ -2780,6 +2806,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz",
"integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
@@ -2794,6 +2821,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz",
"integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.24.7"
},
@@ -2810,6 +2838,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz",
"integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
@@ -2827,6 +2856,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.0.1.tgz",
"integrity": "sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.2.0"
},
@@ -2840,6 +2870,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz",
"integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.11.1",
"@rc-component/motion": "^1.1.4",
@@ -2856,6 +2887,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.3.6.tgz",
"integrity": "sha512-CzbJ9TwmWcF5asvTMZ9BMiTE9CkkrigeOGRPpzCNmeZP7KBwwmYrmOIiKh9tMG7d6DyGAEAQ75LBxzPx+pGTHA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/overflow": "^1.0.0",
"@rc-component/trigger": "^3.0.0",
@@ -2876,6 +2908,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz",
"integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
@@ -2893,6 +2926,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz",
"integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
@@ -2910,6 +2944,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz",
"integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
@@ -2924,6 +2959,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.0.tgz",
"integrity": "sha512-cq3P9FkD+F3eglkFYhBuNlHclg+r4jY8+ZIgK7zbEFo6IwpnA77YL/Gq4ensLw9oua3zFCTA6JDu6YgBei0TxA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/context": "^2.0.1",
"@rc-component/resize-observer": "^1.0.0",
@@ -2944,6 +2980,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz",
"integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/dropdown": "~1.0.0",
"@rc-component/menu": "~1.2.0",
@@ -2965,6 +3002,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz",
"integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/input": "~1.1.0",
"@rc-component/resize-observer": "^1.0.0",
@@ -2981,6 +3019,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz",
"integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/trigger": "^3.7.1",
"@rc-component/util": "^1.3.0",
@@ -2996,6 +3035,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.2.1.tgz",
"integrity": "sha512-BUCrVikGJsXli38qlJ+h2WyDD6dYxzDA9dV3o0ij6gYhAq6ooT08SUMWOikva9v4KZ2BEuluGl5bPcsjrSoBgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/portal": "^2.0.0",
"@rc-component/trigger": "^3.0.0",
@@ -3015,6 +3055,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.1.0.tgz",
"integrity": "sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.0.0",
"@rc-component/util": "^1.2.1",
@@ -3034,6 +3075,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.4.0.tgz",
"integrity": "sha512-I3UAlO2hNqy9CSKc8EBaESgnmKk2QaRzuZ2XHZGFCgsSMkGl06mdF97sVfROM02YIb64ocgLKefsjE0Ch4ocwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/select": "~1.3.0",
"@rc-component/tree": "~1.1.0",
@@ -3050,6 +3092,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.7.2.tgz",
"integrity": "sha512-25x+D2k9SAkaK/MNMNmv2Nlv8FH1D9RtmjoMoLEw1Cid+sMV4pAAT5k49ku59UeXaOA1qwLUVrBUMq4A6gUSsQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.0.0",
@@ -3070,6 +3113,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz",
"integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
@@ -3084,6 +3128,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.6.0.tgz",
"integrity": "sha512-YbjuIVAm8InCnXVoA4n6G+uh31yESTxQ6fSY2frZ2/oMSvktoB+bumFUfNN7RKh7YeOkZgOvN2suGtEDhJSX0A==",
"license": "MIT",
"peer": true,
"dependencies": {
"is-mobile": "^5.0.0",
"react-is": "^18.2.0"
@@ -3098,6 +3143,7 @@
"resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz",
"integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@rc-component/resize-observer": "^1.0.1",
@@ -3645,7 +3691,6 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -4841,7 +4886,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5117,7 +5161,8 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/consola": {
"version": "3.4.0",
@@ -5993,7 +6038,8 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz",
"integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/is-number": {
"version": "7.0.0",
@@ -6077,6 +6123,7 @@
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"string-convert": "^0.2.0"
}
@@ -6172,7 +6219,6 @@
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@@ -6708,7 +6754,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -6721,7 +6766,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -6979,7 +7023,6 @@
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -7084,7 +7127,6 @@
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -7114,6 +7156,7 @@
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
@@ -7211,7 +7254,8 @@
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/string-width": {
"version": "5.1.2",
@@ -7353,6 +7397,7 @@
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.22"
}
@@ -7427,7 +7472,6 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -7448,7 +7492,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7594,7 +7637,6 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -17,7 +17,7 @@ import {
AccordionItemTrigger,
AccordionRoot,
} from "../../components/ui/accordion";
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useShowModal } from "../../components/common/ModalManager";
import {
CommandPrefix_CPUNiceLevel,
@@ -108,6 +108,7 @@ interface SftpConfigSectionProps {
onChangeIdentityFile: (path: string) => void;
port: number | null;
onChangePort: (port: number | null) => void;
knownHostsPath: string;
onChangeKnownHostsPath: (path: string) => void;
isWindows: boolean;
}
@@ -118,70 +119,48 @@ const SftpConfigSection = ({
onChangeIdentityFile,
port,
onChangePort,
knownHostsPath,
onChangeKnownHostsPath,
isWindows,
}: SftpConfigSectionProps) => {
// Setup Keys state
const [sftpUsername, setSftpUsername] = useState("");
const [sftpPassword, setSftpPassword] = useState("");
const [setupLoading, setSetupLoading] = useState(false);
const [generatedPublicKey, setGeneratedPublicKey] = useState<string | null>(
null,
);
const [generatedPublicKey, setGeneratedPublicKey] = useState<string | null>(null);
const [hostKeyWarning, setHostKeyWarning] = useState<string | null>(null);
const [keyCopied, setKeyCopied] = useState(false);
if (isWindows) return null;
const handleSetupKeys = async () => {
const handleGenerateKey = async () => {
setSetupLoading(true);
setGeneratedPublicKey(null);
setHostKeyWarning(null);
try {
if (!uri) return;
// Simple parse of URI for host/port if not fully robust
let host = "";
// Parse host and port from the SFTP URI
const authority = uri.replace("sftp:", "").split("/")[0];
const hostPart = authority.includes("@") ? authority.split("@")[1] : authority;
let host = hostPart;
let defaultPort = "22";
const uriParts = uri.replace("sftp:", "").split("/");
const authority = uriParts[0];
let hostPart = authority;
if (authority.includes("@")) {
setSftpUsername(authority.split("@")[0]);
hostPart = authority.split("@")[1];
}
if (hostPart.includes(":")) {
host = hostPart.split(":")[0];
defaultPort = hostPart.split(":")[1];
} else {
host = hostPart;
[host, defaultPort] = hostPart.split(":");
}
// Override from manual input if username is set there
const username = sftpUsername || uri.match(/([^@]+)@/)?.[1] || "";
const res = await backrestService.setupSftp({
host: host,
host,
port: port ? port.toString() : defaultPort,
username: username,
password: sftpPassword || undefined,
username: "",
});
if (res.error) {
throw new Error(res.error);
}
onChangeIdentityFile(res.keyPath);
onChangeKnownHostsPath(res.knownHostsPath);
if (res.publicKey) {
setGeneratedPublicKey(res.publicKey);
}
alerts.success(
"Created SSH keypair at " +
res.keyPath +
" and updated known hosts file at " +
res.knownHostsPath,
);
alerts.success(
"Updated restic flags to use the SSH keypair and known hosts file.",
);
if (res.error) {
setHostKeyWarning(res.error);
}
alerts.success("Generated SSH keypair at " + res.keyPath);
} catch (e: any) {
alerts.error(formatErrorAlert(e, "SFTP Setup Failed"));
} finally {
@@ -195,34 +174,22 @@ const SftpConfigSection = ({
<AccordionRoot collapsible variant="enclosed">
<AccordionItem value="bootstrap">
<AccordionItemTrigger>
Bootstrap SSH Key (Optional)
Setup SSH Key (Optional)
</AccordionItemTrigger>
<AccordionItemContent>
<Stack gap={3} p={2}>
<CText fontSize="sm">
Enter your SSH credentials here. When you click "Setup Keys",
backrest will generate an SSH key pair.
Click "Generate Key" to create an SSH key pair for this host.
Backrest will attempt to scan the host key into known_hosts automatically.
You will then need to add the generated public key to{" "}
<Code>~/.ssh/authorized_keys</Code> on the remote server.
</CText>
<Field label="SSH Username">
<Input
placeholder="user"
value={sftpUsername}
onChange={(e) => setSftpUsername(e.target.value)}
/>
</Field>
<Field label="SSH Password">
<PasswordInput
placeholder="password (optional)"
value={sftpPassword}
onChange={(e) => setSftpPassword(e.target.value)}
/>
</Field>
<Button
size="sm"
onClick={handleSetupKeys}
onClick={handleGenerateKey}
loading={setupLoading}
>
Setup Keys
Generate Key
</Button>
</Stack>
</AccordionItemContent>
@@ -237,8 +204,7 @@ const SftpConfigSection = ({
Key Generated Successfully!
</CText>
<CText fontSize="sm">
Please add the following public key to your server's{" "}
<Code>~/.ssh/authorized_keys</Code> file:
Add the following public key to <Code>~/.ssh/authorized_keys</Code> on the remote server:
</CText>
<Box position="relative">
<Code
@@ -254,13 +220,22 @@ const SftpConfigSection = ({
size="xs"
onClick={() => {
navigator.clipboard.writeText(generatedPublicKey || "");
alerts.success("Key copied to clipboard");
setKeyCopied(true);
setTimeout(() => setKeyCopied(false), 2000);
}}
colorPalette={keyCopied ? "green" : undefined}
>
Copy
{keyCopied ? "Copied!" : "Copy"}
</Button>
</Box>
</Box>
{hostKeyWarning && (
<Box p={3} borderWidth={1} borderRadius="md" borderColor="yellow.400" bg="yellow.subtle">
<CText fontSize="sm" color="yellow.700">
<strong>Host key scan failed:</strong> {hostKeyWarning}
</CText>
</Box>
)}
</Stack>
</Box>
)}
@@ -288,6 +263,17 @@ const SftpConfigSection = ({
defaultValue={"22"}
/>
</Field>
<Field
label="Known Hosts File"
helperText="Optional: Path to a known_hosts file for host key verification. Populated automatically by Setup Keys."
>
<Input
placeholder="/home/user/.ssh/known_hosts"
value={knownHostsPath}
onChange={(e) => onChangeKnownHostsPath(e.target.value)}
/>
</Field>
</Stack>
);
};
@@ -304,12 +290,14 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
: toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }),
);
// SFTP specific state
// SFTP specific state
const [sftpIdentityFile, setSftpIdentityFile] = useState("");
const [sftpPort, setSftpPort] = useState<number | null>(null);
const [sftpKnownHostsPath, setSftpKnownHostsPath] = useState("");
// Ref to read current flags without making them a useEffect dependency
const flagsRef = useRef<string[]>([]);
const [confirmation, setConfirmation] = useState<ConfirmationState>({
open: false,
title: "",
@@ -323,11 +311,30 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
? toJson(RepoSchema, template, { alwaysEmitImplicit: true })
: toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }),
);
// Reset SFTP fields when template changes (or is null)
if (!template) {
setSftpIdentityFile("");
setSftpPort(null);
setSftpKnownHostsPath("");
setSftpIdentityFile("");
setSftpPort(null);
setSftpKnownHostsPath("");
if (template?.uri?.startsWith("sftp:")) {
// Populate SFTP fields by parsing the existing sftp.args flag
const sftpArgsFlag = (template.flags || []).find(
(f) => f.includes("sftp.args") || f.includes("sftp.command"),
);
if (sftpArgsFlag) {
const argsMatch = sftpArgsFlag.match(/sftp\.args=['"]?(.+?)['"]?\s*$/);
if (argsMatch) {
const argsStr = argsMatch[1].replace(/^'|'$/g, "");
const identityMatch = argsStr.match(/-i\s+["']?([^\s"']+)["']?/);
if (identityMatch) setSftpIdentityFile(identityMatch[1]);
const portMatch = argsStr.match(/-p\s+(\d+)/);
if (portMatch) setSftpPort(parseInt(portMatch[1], 10));
const knownHostsMatch = argsStr.match(
/-oUserKnownHostsFile=["']?([^\s"']+)["']?/,
);
if (knownHostsMatch) setSftpKnownHostsPath(knownHostsMatch[1]);
}
}
}
}, [template]);
@@ -353,49 +360,45 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
return curr;
};
// Logic to update flags based on SFTP inputs
useEffect(() => {
// If we are editing, we don't touch the flags. The user can edit them manually.
if (template) {
return;
}
// Keep flagsRef in sync with latest formData.flags so the SFTP effect can
// read the current value without flags being a reactive dependency.
flagsRef.current = (formData.flags as string[]) || [];
// Keep sftp.args flag in sync with the SFTP config fields.
useEffect(() => {
const uri = getField(["uri"]);
if (!uri?.startsWith("sftp:")) {
return;
}
const currentFlags = getField(["flags"]) || [];
// Read flags via ref so this effect does not re-run whenever the user
// edits the flags list (which would immediately erase empty rows).
const currentFlags = flagsRef.current;
const newFlags = currentFlags.filter(
(f: string) =>
f && !f.includes("sftp.args") && !f.includes("sftp.command"),
);
// Always include -oBatchMode=yes; quote paths to handle spaces.
let sftpArgs = "-oBatchMode=yes";
let argsChanged = false;
if (sftpIdentityFile) {
let cleanPath = sftpIdentityFile;
if (cleanPath.startsWith("@")) {
cleanPath = cleanPath.substring(1);
}
sftpArgs += ` -i ${cleanPath}`;
argsChanged = true;
sftpArgs += ` -i "${cleanPath}"`;
}
if (sftpPort && sftpPort !== 0 && sftpPort !== 22) {
sftpArgs += ` -p ${sftpPort}`;
argsChanged = true;
}
if (sftpKnownHostsPath) {
sftpArgs += ` -oUserKnownHostsFile=${sftpKnownHostsPath}`;
argsChanged = true;
sftpArgs += ` -oUserKnownHostsFile="${sftpKnownHostsPath}"`;
}
if (argsChanged) {
newFlags.push(`--option=sftp.args='${sftpArgs}'`);
}
newFlags.push(`--option=sftp.args='${sftpArgs}'`);
const sortedCurrent = [...currentFlags].sort();
const sortedNew = [...newFlags].sort();
@@ -407,8 +410,9 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
getField(["uri"]),
sftpIdentityFile,
sftpPort,
template,
getField(["flags"]),
sftpKnownHostsPath,
// flags intentionally omitted: flagsRef avoids a circular dep where any
// user edit to flags would re-trigger the effect and erase empty rows.
]);
if (!config) return null;
@@ -784,13 +788,14 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
</Field>
{/* SFTP Specific Fields */}
{getField(["uri"])?.startsWith("sftp:") && !template && (
{getField(["uri"])?.startsWith("sftp:") && (
<SftpConfigSection
uri={getField(["uri"])}
identityFile={sftpIdentityFile}
onChangeIdentityFile={setSftpIdentityFile}
port={sftpPort}
onChangePort={setSftpPort}
knownHostsPath={sftpKnownHostsPath}
onChangeKnownHostsPath={setSftpKnownHostsPath}
isWindows={isWindows}
/>