pico
created pr with
47.1
added 47.2
1: 0306c3f < -: ------- chore: rsync test
2: 971b354 ! 1: 745b513 chore: add `rsync --delete` test
added 47.3
1: 745b513 ! 1: 76a6734 chore: add `rsync --delete` test
added 47.4
1: 76a6734 ! 1: 57bb60c chore: add `rsync --delete` test
added 47.5
1: 57bb60c = 1: 57bb60c chore: add `rsync --delete` test
-: ------- > 2: 5c8d929 chore: test is failing on `--delete`
changed status to
accepted
cmds
checkout latest patchset:
ssh pr.pico.sh print 47 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 47.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 47set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 47set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 47
Patchset
47.2
chore: add `rsync --delete` test
Eric Bower
2025-02-17T15:36:03ZI can't figure out how to format the `exec.Command` for rsync with `-e`
flag
Current error:
```
[rsync -rv -e "ssh -p 2222 -o IdentitiesOnly=yes -i /tmp/rsync-3246995037/id_ed25519 -o StrictHostKeyChecking=no" /tmp/rsync-3246995037/ localhost:/test]
rsync: [sender] Failed to exec ssh -p 2222 -o IdentitiesOnly=yes -i /tmp/rsync-3246995037/id_ed25519 -o StrictHostKeyChecking=no: No such file or directory (2)
rsync error: error in IPC code (code 14) at pipe.c(85) [sender=3.4.0]
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in IPC code (code 14) at io.c(232) [sender=3.4.0]
exit status 14
--- FAIL: TestSshServerRsync (0.10s)
ssh_test.go:152: exit status 14
```
Semantic diff summary
4 added,
4 modified,
0 signature changed,
1 removed
across 2 analyzed files
+153
-5
pgs/ssh_test.go
#
| ... | ... | @@ -57,9 +62,142 @@ func TestSshServer(t *testing.T) { | |
| 57 | 62 | done <- nil | |
| 58 | 63 | } | |
| 59 | 64 | ||
| 65 | + | func TestSshServerRsync(t *testing.T) { | |
| 66 | + | logger := slog.Default() | |
| 67 | + | dbpool := pgsdb.NewDBMemory(logger) | |
| 68 | + | // setup test data | |
| 69 | + | dbpool.SetupTestData() | |
| 70 | + | st, err := storage.NewStorageMemory(map[string]map[string]string{}) | |
| 71 | + | if err != nil { | |
| 72 | + | panic(err) | |
| 73 | + | } | |
| 74 | + | cfg := NewPgsConfig(logger, dbpool, st) | |
| 75 | + | done := make(chan error) | |
| 76 | + | go StartSshServer(cfg, done) | |
| 77 | + | // Hack to wait for startup | |
| 78 | + | time.Sleep(time.Millisecond * 100) | |
| 79 | + | ||
| 80 | + | user := GenerateUser() | |
| 81 | + | key := utils.KeyForKeyText(user.signer.PublicKey()) | |
| 82 | + | // add user's pubkey to the default test account | |
| 83 | + | dbpool.Pubkeys = append(dbpool.Pubkeys, &db.PublicKey{ | |
| 84 | + | ID: "nice-pubkey", | |
| 85 | + | UserID: dbpool.Users[0].ID, | |
| 86 | + | Key: key, | |
| 87 | + | }) | |
| 88 | + | ||
| 89 | + | conn, err := user.NewClient() | |
| 90 | + | if err != nil { | |
| 91 | + | t.Error(err) | |
| 92 | + | return | |
| 93 | + | } | |
| 94 | + | defer conn.Close() | |
| 95 | + | ||
| 96 | + | // open an SFTP session over an existing ssh connection. | |
| 97 | + | client, err := sftp.NewClient(conn) | |
| 98 | + | if err != nil { | |
| 99 | + | cfg.Logger.Error("could not create sftp client", "err", err) | |
| 100 | + | panic(err) | |
| 101 | + | } | |
| 102 | + | defer client.Close() | |
| 103 | + | ||
| 104 | + | name, err := os.MkdirTemp("", "rsync-") | |
| 105 | + | if err != nil { | |
| 106 | + | panic(err) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // remove the temporary directory at the end of the program | |
| 110 | + | defer os.RemoveAll(name) | |
| 111 | + | ||
| 112 | + | block := &pem.Block{ | |
| 113 | + | Type: "PRIVATE KEY", | |
| 114 | + | Bytes: user.privateKey, | |
| 115 | + | } | |
| 116 | + | keyFile := filepath.Join(name, "id_ed25519") | |
| 117 | + | err = os.WriteFile( | |
| 118 | + | keyFile, | |
| 119 | + | pem.EncodeToMemory(block), 0600, | |
| 120 | + | ) | |
| 121 | + | ||
| 122 | + | index := "<!doctype html><html><body>index</body></html>" | |
| 123 | + | err = os.WriteFile( | |
| 124 | + | filepath.Join(name, "index.html"), | |
| 125 | + | []byte(index), 0666, | |
| 126 | + | ) | |
| 127 | + | ||
| 128 | + | about := "<!doctype html><html><body>about</body></html>" | |
| 129 | + | aboutFile := filepath.Join(name, "about.html") | |
| 130 | + | err = os.WriteFile( | |
| 131 | + | aboutFile, | |
| 132 | + | []byte(about), 0666, | |
| 133 | + | ) | |
| 134 | + | ||
| 135 | + | contact := "<!doctype html><html><body>contact</body></html>" | |
| 136 | + | err = os.WriteFile( | |
| 137 | + | filepath.Join(name, "contact.html"), | |
| 138 | + | []byte(contact), 0666, | |
| 139 | + | ) | |
| 140 | + | ||
| 141 | + | eCmd := fmt.Sprintf( | |
| 142 | + | `"ssh -p 2222 -o IdentitiesOnly=yes -i %s -o StrictHostKeyChecking=no"`, | |
| 143 | + | keyFile, | |
| 144 | + | ) | |
| 145 | + | ||
| 146 | + | // copy files | |
| 147 | + | cmd := exec.Command("rsync", "-rv", "-e", eCmd, name+"/", "localhost:/test") | |
| 148 | + | fmt.Println(cmd.Args) | |
| 149 | + | result, err := cmd.CombinedOutput() | |
| 150 | + | if err != nil { | |
| 151 | + | fmt.Println(string(result), err) | |
| 152 | + | t.Error(err) | |
| 153 | + | return | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // check it's there | |
| 157 | + | fi, err := client.Lstat("about.html") | |
| 158 | + | if err != nil { | |
| 159 | + | cfg.Logger.Error("could not get stat for file", "err", err) | |
| 160 | + | t.Error("about.html not found") | |
| 161 | + | return | |
| 162 | + | } | |
| 163 | + | if fi.Size() != 0 { | |
| 164 | + | cfg.Logger.Error("about.html wrong size", "size", fi.Size()) | |
| 165 | + | t.Error("about.html wrong size") | |
| 166 | + | return | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | // remove about file | |
| 170 | + | os.Remove(aboutFile) | |
| 171 | + | ||
| 172 | + | // copy files with delete | |
| 173 | + | delCmd := exec.Command("rsync", "-rv", "--delete", "-e", eCmd, name+"/", "localhost:/test") | |
| 174 | + | err = delCmd.Run() | |
| 175 | + | if err != nil { | |
| 176 | + | t.Error(err) | |
| 177 | + | return | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | done <- nil | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func createTmpFile(name, contents, ext string) *os.File { | |
| 184 | + | file, err := os.CreateTemp("tmp", fmt.Sprintf("%s-*.%s", name, ext)) | |
| 185 | + | if err != nil { | |
| 186 | + | panic(err) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | data := []byte(contents) | |
| 190 | + | if _, err := file.Write(data); err != nil { | |
| 191 | + | panic(err) | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | return file | |
| 195 | + | } | |
| 196 | + | ||
| 60 | 197 | type UserSSH struct { | |
| 61 | - | username string | |
| 62 | - | signer ssh.Signer | |
| 198 | + | username string | |
| 199 | + | signer ssh.Signer | |
| 200 | + | privateKey []byte | |
| 63 | 201 | } | |
| 64 | 202 | ||
| 65 | 203 | func NewUserSSH(username string, signer ssh.Signer) *UserSSH { |
| ... | ... | @@ -146,17 +284,27 @@ func GenerateUser() UserSSH { | |
| 146 | 284 | panic(err) | |
| 147 | 285 | } | |
| 148 | 286 | ||
| 287 | + | b, err := x509.MarshalPKCS8PrivateKey(userKey) | |
| 288 | + | if err != nil { | |
| 289 | + | panic(err) | |
| 290 | + | } | |
| 291 | + | ||
| 149 | 292 | userSigner, err := ssh.NewSignerFromKey(userKey) | |
| 150 | 293 | if err != nil { | |
| 151 | 294 | panic(err) | |
| 152 | 295 | } | |
| 153 | 296 | ||
| 154 | 297 | return UserSSH{ | |
| 155 | - | username: "testuser", | |
| 156 | - | signer: userSigner, | |
| 298 | + | username: "testuser", | |
| 299 | + | signer: userSigner, | |
| 300 | + | privateKey: b, | |
| 157 | 301 | } | |
| 158 | 302 | } | |
| 159 | 303 | ||
| 304 | + | func WriteFilesWithRsync(cfg *PgsConfig, conn *ssh.Client, files []string) (*os.FileInfo, error) { | |
| 305 | + | return nil, nil | |
| 306 | + | } | |
| 307 | + | ||
| 160 | 308 | func WriteFileWithSftp(cfg *PgsConfig, conn *ssh.Client) (*os.FileInfo, error) { | |
| 161 | 309 | // open an SFTP session over an existing ssh connection. | |
| 162 | 310 | client, err := sftp.NewClient(conn) |