raycast-rsync-extension 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +105 -0
- package/.github/workflows/publish.yml +269 -0
- package/.github/workflows/update-copyright-year.yml +70 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/assets/icon.png +0 -0
- package/eslint.config.js +23 -0
- package/metadata/browse-remote-path.png +0 -0
- package/metadata/browse-remote.png +0 -0
- package/metadata/download-local-path.png +0 -0
- package/metadata/download-remote-path.png +0 -0
- package/metadata/extension.png +0 -0
- package/metadata/upload-local-path.png +0 -0
- package/metadata/upload-remote-path.png +0 -0
- package/metadata/upload-search-host.png +0 -0
- package/package.json +93 -0
- package/src/__mocks__/raycast-api.ts +84 -0
- package/src/browse.tsx +378 -0
- package/src/components/FileList.test.tsx +73 -0
- package/src/components/FileList.tsx +61 -0
- package/src/download.tsx +353 -0
- package/src/e2e/browse.e2e.test.ts +295 -0
- package/src/e2e/download.e2e.test.ts +193 -0
- package/src/e2e/error-handling.e2e.test.ts +292 -0
- package/src/e2e/rsync-options.e2e.test.ts +348 -0
- package/src/e2e/upload.e2e.test.ts +207 -0
- package/src/index.tsx +21 -0
- package/src/test-setup.ts +1 -0
- package/src/types/server.ts +60 -0
- package/src/upload.tsx +404 -0
- package/src/utils/__tests__/sshConfig.test.ts +352 -0
- package/src/utils/__tests__/validation.test.ts +139 -0
- package/src/utils/preferences.ts +24 -0
- package/src/utils/rsync.test.ts +490 -0
- package/src/utils/rsync.ts +517 -0
- package/src/utils/shellEscape.test.ts +98 -0
- package/src/utils/shellEscape.ts +36 -0
- package/src/utils/ssh.test.ts +209 -0
- package/src/utils/ssh.ts +187 -0
- package/src/utils/sshConfig.test.ts +191 -0
- package/src/utils/sshConfig.ts +212 -0
- package/src/utils/validation.test.ts +224 -0
- package/src/utils/validation.ts +115 -0
- package/tsconfig.json +27 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { buildRsyncCommand } from "./rsync";
|
|
3
|
+
import {
|
|
4
|
+
TransferOptions,
|
|
5
|
+
TransferDirection,
|
|
6
|
+
SSHHostConfig,
|
|
7
|
+
} from "../types/server";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { statSync } from "fs";
|
|
11
|
+
import type { PathLike } from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
|
|
15
|
+
vi.mock("fs", async () => {
|
|
16
|
+
const actual = await vi.importActual<typeof import("fs")>("fs");
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
statSync: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Rsync Command Builder", () => {
|
|
24
|
+
const mockHostConfig: SSHHostConfig = {
|
|
25
|
+
host: "testserver",
|
|
26
|
+
hostName: "example.com",
|
|
27
|
+
user: "testuser",
|
|
28
|
+
port: 22,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const configPath = join(homedir(), ".ssh", "config");
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Mock statSync to return file stats by default
|
|
35
|
+
vi.mocked(statSync).mockImplementation((path: PathLike) => {
|
|
36
|
+
const pathStr = path.toString();
|
|
37
|
+
// Return directory stats for paths ending with "directory" or containing "/dir"
|
|
38
|
+
if (
|
|
39
|
+
pathStr.includes("/directory") ||
|
|
40
|
+
pathStr.endsWith("directory") ||
|
|
41
|
+
pathStr.includes("/dir/") ||
|
|
42
|
+
pathStr === "/local/dir"
|
|
43
|
+
) {
|
|
44
|
+
return {
|
|
45
|
+
isDirectory: () => true,
|
|
46
|
+
isFile: () => false,
|
|
47
|
+
} as ReturnType<typeof statSync>;
|
|
48
|
+
}
|
|
49
|
+
// Return file stats for other paths
|
|
50
|
+
return {
|
|
51
|
+
isDirectory: () => false,
|
|
52
|
+
isFile: () => true,
|
|
53
|
+
} as ReturnType<typeof statSync>;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("buildRsyncCommand", () => {
|
|
62
|
+
it("should construct upload command with correct format", () => {
|
|
63
|
+
const options: TransferOptions = {
|
|
64
|
+
hostConfig: mockHostConfig,
|
|
65
|
+
localPath: "/local/path/file.txt",
|
|
66
|
+
remotePath: "/remote/path/file.txt",
|
|
67
|
+
direction: TransferDirection.UPLOAD,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const command = buildRsyncCommand(options);
|
|
71
|
+
|
|
72
|
+
// Paths should be properly escaped
|
|
73
|
+
expect(command).toContain("'/local/path/file.txt'");
|
|
74
|
+
expect(command).toContain("'testserver':");
|
|
75
|
+
expect(command).toContain("'/remote/path/file.txt'");
|
|
76
|
+
expect(command).toMatch(/rsync -e '/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should construct download command with correct format", () => {
|
|
80
|
+
const options: TransferOptions = {
|
|
81
|
+
hostConfig: mockHostConfig,
|
|
82
|
+
localPath: "/local/path/destination",
|
|
83
|
+
remotePath: "/remote/path/file.txt",
|
|
84
|
+
direction: TransferDirection.DOWNLOAD,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const command = buildRsyncCommand(options);
|
|
88
|
+
|
|
89
|
+
// Paths should be properly escaped
|
|
90
|
+
// For downloads, local destination should have trailing slash to ensure directory is created
|
|
91
|
+
expect(command).toContain("'/local/path/destination/'");
|
|
92
|
+
expect(command).toContain("'testserver':");
|
|
93
|
+
expect(command).toContain("'/remote/path/file.txt'");
|
|
94
|
+
expect(command).toMatch(/rsync -e '/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should include archive flag in command", () => {
|
|
98
|
+
const options: TransferOptions = {
|
|
99
|
+
hostConfig: mockHostConfig,
|
|
100
|
+
localPath: "/local/directory",
|
|
101
|
+
remotePath: "/remote/directory",
|
|
102
|
+
direction: TransferDirection.UPLOAD,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const command = buildRsyncCommand(options);
|
|
106
|
+
|
|
107
|
+
expect(command).toContain("-a");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should include SSH config in command", () => {
|
|
111
|
+
const options: TransferOptions = {
|
|
112
|
+
hostConfig: mockHostConfig,
|
|
113
|
+
localPath: "/local/path",
|
|
114
|
+
remotePath: "/remote/path",
|
|
115
|
+
direction: TransferDirection.UPLOAD,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const command = buildRsyncCommand(options);
|
|
119
|
+
|
|
120
|
+
// SSH command should be escaped
|
|
121
|
+
expect(command).toMatch(/rsync -e '/);
|
|
122
|
+
expect(command).toContain(configPath);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should use host alias in command", () => {
|
|
126
|
+
const options: TransferOptions = {
|
|
127
|
+
hostConfig: mockHostConfig,
|
|
128
|
+
localPath: "/local/path",
|
|
129
|
+
remotePath: "/remote/path",
|
|
130
|
+
direction: TransferDirection.UPLOAD,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const command = buildRsyncCommand(options);
|
|
134
|
+
|
|
135
|
+
// Host alias should be escaped
|
|
136
|
+
expect(command).toContain("'testserver':");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle different host aliases", () => {
|
|
140
|
+
const customHostConfig: SSHHostConfig = {
|
|
141
|
+
host: "production-server",
|
|
142
|
+
hostName: "prod.example.com",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const options: TransferOptions = {
|
|
146
|
+
hostConfig: customHostConfig,
|
|
147
|
+
localPath: "/local/file",
|
|
148
|
+
remotePath: "/remote/file",
|
|
149
|
+
direction: TransferDirection.DOWNLOAD,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const command = buildRsyncCommand(options);
|
|
153
|
+
|
|
154
|
+
// Host alias should be escaped
|
|
155
|
+
expect(command).toContain("'production-server':");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should prevent command injection in localPath", () => {
|
|
159
|
+
const options: TransferOptions = {
|
|
160
|
+
hostConfig: mockHostConfig,
|
|
161
|
+
localPath: "/tmp/test; rm -rf /",
|
|
162
|
+
remotePath: "/remote/path",
|
|
163
|
+
direction: TransferDirection.UPLOAD,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const command = buildRsyncCommand(options);
|
|
167
|
+
|
|
168
|
+
// The malicious command should be escaped, not executed
|
|
169
|
+
expect(command).toContain("'/tmp/test; rm -rf /'");
|
|
170
|
+
// The semicolon should be inside single quotes (escaped), not outside
|
|
171
|
+
// Check that the path is properly quoted
|
|
172
|
+
expect(command).toMatch(/'\/(tmp|local)\/test; rm -rf \/'/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should prevent command injection in remotePath", () => {
|
|
176
|
+
const options: TransferOptions = {
|
|
177
|
+
hostConfig: mockHostConfig,
|
|
178
|
+
localPath: "/local/path",
|
|
179
|
+
remotePath: "/tmp/test | cat /etc/passwd",
|
|
180
|
+
direction: TransferDirection.UPLOAD,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const command = buildRsyncCommand(options);
|
|
184
|
+
|
|
185
|
+
// The malicious command should be escaped
|
|
186
|
+
expect(command).toContain("'/tmp/test | cat /etc/passwd'");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should prevent command injection in hostAlias", () => {
|
|
190
|
+
const maliciousHostConfig: SSHHostConfig = {
|
|
191
|
+
host: "server; rm -rf /",
|
|
192
|
+
hostName: "example.com",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const options: TransferOptions = {
|
|
196
|
+
hostConfig: maliciousHostConfig,
|
|
197
|
+
localPath: "/local/path",
|
|
198
|
+
remotePath: "/remote/path",
|
|
199
|
+
direction: TransferDirection.UPLOAD,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const command = buildRsyncCommand(options);
|
|
203
|
+
|
|
204
|
+
// The malicious host alias should be escaped
|
|
205
|
+
expect(command).toContain("'server; rm -rf /':");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should handle paths with spaces", () => {
|
|
209
|
+
const options: TransferOptions = {
|
|
210
|
+
hostConfig: mockHostConfig,
|
|
211
|
+
localPath: "/local/path with spaces/file.txt",
|
|
212
|
+
remotePath: "/remote/path with spaces/file.txt",
|
|
213
|
+
direction: TransferDirection.UPLOAD,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const command = buildRsyncCommand(options);
|
|
217
|
+
|
|
218
|
+
// Paths with spaces should be properly escaped
|
|
219
|
+
expect(command).toContain("'/local/path with spaces/file.txt'");
|
|
220
|
+
expect(command).toContain("'/remote/path with spaces/file.txt'");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should handle paths with single quotes", () => {
|
|
224
|
+
const options: TransferOptions = {
|
|
225
|
+
hostConfig: mockHostConfig,
|
|
226
|
+
localPath: "/local/file'name.txt",
|
|
227
|
+
remotePath: "/remote/file'name.txt",
|
|
228
|
+
direction: TransferDirection.UPLOAD,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const command = buildRsyncCommand(options);
|
|
232
|
+
|
|
233
|
+
// Paths with single quotes should be properly escaped
|
|
234
|
+
expect(command).toContain("'/local/file'\\''name.txt'");
|
|
235
|
+
expect(command).toContain("'/remote/file'\\''name.txt'");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should include human-readable flag when enabled", () => {
|
|
239
|
+
const options: TransferOptions = {
|
|
240
|
+
hostConfig: mockHostConfig,
|
|
241
|
+
localPath: "/local/path",
|
|
242
|
+
remotePath: "/remote/path",
|
|
243
|
+
direction: TransferDirection.UPLOAD,
|
|
244
|
+
rsyncOptions: {
|
|
245
|
+
humanReadable: true,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const command = buildRsyncCommand(options);
|
|
250
|
+
|
|
251
|
+
// Flag should be included in the combined flags string (e.g., -avzh)
|
|
252
|
+
expect(command).toMatch(/-[avz]+h/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should include progress flag when enabled", () => {
|
|
256
|
+
const options: TransferOptions = {
|
|
257
|
+
hostConfig: mockHostConfig,
|
|
258
|
+
localPath: "/local/path",
|
|
259
|
+
remotePath: "/remote/path",
|
|
260
|
+
direction: TransferDirection.UPLOAD,
|
|
261
|
+
rsyncOptions: {
|
|
262
|
+
progress: true,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const command = buildRsyncCommand(options);
|
|
267
|
+
|
|
268
|
+
// Flag should be included in the combined flags string (e.g., -avzP)
|
|
269
|
+
expect(command).toMatch(/-[avz]+P/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should include delete flag when enabled", () => {
|
|
273
|
+
const options: TransferOptions = {
|
|
274
|
+
hostConfig: mockHostConfig,
|
|
275
|
+
localPath: "/local/path",
|
|
276
|
+
remotePath: "/remote/path",
|
|
277
|
+
direction: TransferDirection.UPLOAD,
|
|
278
|
+
rsyncOptions: {
|
|
279
|
+
delete: true,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const command = buildRsyncCommand(options);
|
|
284
|
+
|
|
285
|
+
expect(command).toContain("--delete");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should include all optional flags when all enabled", () => {
|
|
289
|
+
const options: TransferOptions = {
|
|
290
|
+
hostConfig: mockHostConfig,
|
|
291
|
+
localPath: "/local/path",
|
|
292
|
+
remotePath: "/remote/path",
|
|
293
|
+
direction: TransferDirection.DOWNLOAD,
|
|
294
|
+
rsyncOptions: {
|
|
295
|
+
humanReadable: true,
|
|
296
|
+
progress: true,
|
|
297
|
+
delete: true,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const command = buildRsyncCommand(options);
|
|
302
|
+
|
|
303
|
+
// Combined short flags should include h and P (e.g., -avzhP)
|
|
304
|
+
expect(command).toMatch(/-[avz]+hP/);
|
|
305
|
+
expect(command).toContain("--delete");
|
|
306
|
+
// Base flags should be present
|
|
307
|
+
expect(command).toMatch(/-[avz]+/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should not include optional flags when disabled", () => {
|
|
311
|
+
const options: TransferOptions = {
|
|
312
|
+
hostConfig: mockHostConfig,
|
|
313
|
+
localPath: "/local/path",
|
|
314
|
+
remotePath: "/remote/path",
|
|
315
|
+
direction: TransferDirection.UPLOAD,
|
|
316
|
+
rsyncOptions: {
|
|
317
|
+
humanReadable: false,
|
|
318
|
+
progress: false,
|
|
319
|
+
delete: false,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const command = buildRsyncCommand(options);
|
|
324
|
+
|
|
325
|
+
// Extract the flags part (between -e and paths)
|
|
326
|
+
// Note: -e argument is now escaped with single quotes
|
|
327
|
+
const flagsMatch = command.match(/rsync -e '[^']+' (-[^ ]+)/);
|
|
328
|
+
expect(flagsMatch).not.toBeNull();
|
|
329
|
+
const flags = flagsMatch![1];
|
|
330
|
+
|
|
331
|
+
// Optional flags should not be present
|
|
332
|
+
expect(flags).not.toContain("h");
|
|
333
|
+
expect(flags).not.toContain("P");
|
|
334
|
+
expect(command).not.toContain("--delete");
|
|
335
|
+
// Base flags should still be present
|
|
336
|
+
expect(flags).toContain("a");
|
|
337
|
+
expect(flags).toContain("v");
|
|
338
|
+
expect(flags).toContain("z");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should normalize directory paths for upload - remove trailing slash from source, add to destination", () => {
|
|
342
|
+
const options: TransferOptions = {
|
|
343
|
+
hostConfig: mockHostConfig,
|
|
344
|
+
localPath: "/local/directory",
|
|
345
|
+
remotePath: "/remote/path",
|
|
346
|
+
direction: TransferDirection.UPLOAD,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const command = buildRsyncCommand(options);
|
|
350
|
+
|
|
351
|
+
// Source directory should not have trailing slash
|
|
352
|
+
expect(command).toContain("'/local/directory'");
|
|
353
|
+
expect(command).not.toContain("'/local/directory/'");
|
|
354
|
+
// Destination should have trailing slash
|
|
355
|
+
expect(command).toContain("'/remote/path/'");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should normalize directory paths for upload when source has trailing slash", () => {
|
|
359
|
+
const options: TransferOptions = {
|
|
360
|
+
hostConfig: mockHostConfig,
|
|
361
|
+
localPath: "/local/directory/",
|
|
362
|
+
remotePath: "/remote/path",
|
|
363
|
+
direction: TransferDirection.UPLOAD,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const command = buildRsyncCommand(options);
|
|
367
|
+
|
|
368
|
+
// Source directory should not have trailing slash (removed)
|
|
369
|
+
expect(command).toContain("'/local/directory'");
|
|
370
|
+
expect(command).not.toContain("'/local/directory/'");
|
|
371
|
+
// Destination should have trailing slash
|
|
372
|
+
expect(command).toContain("'/remote/path/'");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should not modify file paths for upload", () => {
|
|
376
|
+
const options: TransferOptions = {
|
|
377
|
+
hostConfig: mockHostConfig,
|
|
378
|
+
localPath: "/local/path/file.txt",
|
|
379
|
+
remotePath: "/remote/path/file.txt",
|
|
380
|
+
direction: TransferDirection.UPLOAD,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const command = buildRsyncCommand(options);
|
|
384
|
+
|
|
385
|
+
// File paths should remain unchanged
|
|
386
|
+
expect(command).toContain("'/local/path/file.txt'");
|
|
387
|
+
expect(command).toContain("'/remote/path/file.txt'");
|
|
388
|
+
expect(command).not.toContain("'/remote/path/file.txt/'");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should normalize paths for download - add trailing slash to local destination, remove from remote source", () => {
|
|
392
|
+
const options: TransferOptions = {
|
|
393
|
+
hostConfig: mockHostConfig,
|
|
394
|
+
localPath: "/local/path/destination",
|
|
395
|
+
remotePath: "/remote/directory",
|
|
396
|
+
direction: TransferDirection.DOWNLOAD,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const command = buildRsyncCommand(options);
|
|
400
|
+
|
|
401
|
+
// Local destination should have trailing slash
|
|
402
|
+
expect(command).toContain("'/local/path/destination/'");
|
|
403
|
+
// Remote source should not have trailing slash
|
|
404
|
+
expect(command).toContain("'/remote/directory'");
|
|
405
|
+
expect(command).not.toContain("'/remote/directory/'");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should normalize paths for download when remote has trailing slash", () => {
|
|
409
|
+
const options: TransferOptions = {
|
|
410
|
+
hostConfig: mockHostConfig,
|
|
411
|
+
localPath: "/local/path/destination",
|
|
412
|
+
remotePath: "/remote/directory/",
|
|
413
|
+
direction: TransferDirection.DOWNLOAD,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const command = buildRsyncCommand(options);
|
|
417
|
+
|
|
418
|
+
// Local destination should have trailing slash
|
|
419
|
+
expect(command).toContain("'/local/path/destination/'");
|
|
420
|
+
// Remote source should not have trailing slash (removed)
|
|
421
|
+
expect(command).toContain("'/remote/directory'");
|
|
422
|
+
expect(command).not.toContain("'/remote/directory/'");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should handle root path correctly", () => {
|
|
426
|
+
const options: TransferOptions = {
|
|
427
|
+
hostConfig: mockHostConfig,
|
|
428
|
+
localPath: "/",
|
|
429
|
+
remotePath: "/remote/path",
|
|
430
|
+
direction: TransferDirection.UPLOAD,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const command = buildRsyncCommand(options);
|
|
434
|
+
|
|
435
|
+
// Root path should remain as "/" (not modified)
|
|
436
|
+
expect(command).toContain("'/'");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("should expand tilde in local paths for upload", () => {
|
|
440
|
+
const options: TransferOptions = {
|
|
441
|
+
hostConfig: mockHostConfig,
|
|
442
|
+
localPath: "~/Documents/file.txt",
|
|
443
|
+
remotePath: "/remote/path",
|
|
444
|
+
direction: TransferDirection.UPLOAD,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const command = buildRsyncCommand(options);
|
|
448
|
+
|
|
449
|
+
// Tilde should be expanded to home directory (use homedir() to get actual path)
|
|
450
|
+
const expectedPath = path.join(os.homedir(), "Documents/file.txt");
|
|
451
|
+
expect(command).toContain(`'${expectedPath}'`);
|
|
452
|
+
// Should not contain literal ~
|
|
453
|
+
expect(command).not.toContain("'~/");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("should expand tilde in local paths for download", () => {
|
|
457
|
+
const options: TransferOptions = {
|
|
458
|
+
hostConfig: mockHostConfig,
|
|
459
|
+
localPath: "~/Desktop",
|
|
460
|
+
remotePath: "/remote/directory",
|
|
461
|
+
direction: TransferDirection.DOWNLOAD,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const command = buildRsyncCommand(options);
|
|
465
|
+
|
|
466
|
+
// Tilde should be expanded to home directory with trailing slash
|
|
467
|
+
const expectedPath = path.join(os.homedir(), "Desktop") + "/";
|
|
468
|
+
expect(command).toContain(`'${expectedPath}'`);
|
|
469
|
+
// Should not contain literal ~
|
|
470
|
+
expect(command).not.toContain("'~/");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("should expand standalone tilde to home directory", () => {
|
|
474
|
+
const options: TransferOptions = {
|
|
475
|
+
hostConfig: mockHostConfig,
|
|
476
|
+
localPath: "~",
|
|
477
|
+
remotePath: "/remote/path",
|
|
478
|
+
direction: TransferDirection.DOWNLOAD,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const command = buildRsyncCommand(options);
|
|
482
|
+
|
|
483
|
+
// Standalone tilde should be expanded to home directory with trailing slash
|
|
484
|
+
const expectedPath = os.homedir() + "/";
|
|
485
|
+
expect(command).toContain(`'${expectedPath}'`);
|
|
486
|
+
// Should not contain literal ~
|
|
487
|
+
expect(command).not.toContain("'~'");
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|