Compare commits

..
Author SHA1 Message Date
陈大猫 46df6f3b93 fix(sftp): expose lstat on session-backed copy clients 2026-11-13 16:29:40 +08:00
陈大猫 32130a560e test(sftp): mock missing dest in same-host cp cache fixture 2026-11-13 15:20:52 +08:00
陈大猫 b28a2bf1d8 fix(sftp): skip same-host cp for symlinks and directory merges 2026-11-13 14:55:19 +08:00
陈大猫 cd6334e150 fix(sftp): preserve dest mode when same-host cp replaces a file 2026-11-13 14:20:17 +08:00
陈大猫 aa4583050d fix(sftp): skip same-host directory cp when auto encoding resolves to non-UTF-8 2026-11-13 13:48:33 +08:00
陈大猫 37188871d8 fix(sftp): use remote cp for fresh same-host copies (Fixes #4207)
Ordinary SFTP tasks were marked resumable, which skipped the existing file and directory cp optimizations. Select the non-resumable route only when creating fresh matching-host copies with eligible encodings, keeping transfer-center rows and recursive children consistent. Existing checkpointed transfers retain their saved resumable path; failed cp attempts retain the download/upload fallback.
2026-11-13 13:28:47 +08:00
7 changed files with 3 additions and 137 deletions

No files matched your search

@@ -9,13 +9,6 @@ test("SFTP settings imports the shared Toggle used by its option rows", () => {
assert.match(source, /Toggle,\s*\n\s*Select,/);
});
test("folder compression row exposes its settings search anchor", () => {
assert.match(
source,
/<SettingRow\s+anchorId="sftp-compressed-upload"\s+label=\{t\('settings\.sftp\.compressedUpload\.enable'\)\}/,
);
});
function functionBody(functionName: string): string {
const start = source.indexOf(`const ${functionName} = useCallback`);
assert.notEqual(start, -1, `${functionName} should exist`);
@@ -180,7 +180,6 @@ export default function SettingsFileAssociationsTab() {
<SectionHeader title={t('settings.sftp.compressedUpload')} />
<SettingCard>
<SettingRow
anchorId="sftp-compressed-upload"
label={t('settings.sftp.compressedUpload.enable')}
description={t('settings.sftp.compressedUpload.enableDesc')}
>
@@ -232,14 +231,12 @@ export default function SettingsFileAssociationsTab() {
</div>
</SettingRow>
<SettingRow
anchorId="sftp-skip-unchanged"
label={t('settings.sftp.skipUnchanged.enable')}
description={t('settings.sftp.skipUnchanged.enableDesc')}
>
<Toggle checked={sftpSkipUnchanged} onChange={setSftpSkipUnchanged} />
</SettingRow>
<SettingRow
anchorId="sftp-transport-idle-ttl"
label={t('settings.ssh.transportIdleTtl')}
description={t('settings.ssh.transportIdleTtl.desc')}
>
-42
View File
@@ -125,48 +125,6 @@ test("settings search finds App Lock from the zh-CN locale", () => {
assert.ok(hits.some((hit) => hit.entry.id === "system-app-lock"));
});
test("settings search finds folder compression from the real locales", () => {
const tZhReal = (key: string) => zhCN[key] ?? key;
const tEnReal = (key: string) => en[key] ?? key;
for (const [t, queries] of [
[tZhReal, ["压缩", "普通传输"]],
[tEnReal, ["compress", "tar"]],
] as const) {
for (const query of queries) {
const hits = filterSettingsSearchCatalog(query, t);
assert.ok(hits.some((hit) => hit.entry.id === "sftp-compressed-upload"), query);
}
}
});
test("settings search finds skip unchanged files from the real locales", () => {
const tZhReal = (key: string) => zhCN[key] ?? key;
const tEnReal = (key: string) => en[key] ?? key;
for (const [t, queries] of [
[tZhReal, ["跳过未改文件", "修改时间"]],
[tEnReal, ["skip unchanged", "rsync"]],
] as const) {
for (const query of queries) {
const hits = filterSettingsSearchCatalog(query, t);
assert.ok(hits.some((hit) => hit.entry.id === "sftp-skip-unchanged"), query);
}
}
});
test("settings search finds SSH connection keep-alive from the real locales", () => {
const tZhReal = (key: string) => zhCN[key] ?? key;
const tEnReal = (key: string) => en[key] ?? key;
for (const [t, queries] of [
[tZhReal, ["保活时间", "重新认证"]],
[tEnReal, ["keep-alive", "re-authentication"]],
] as const) {
for (const query of queries) {
const hits = filterSettingsSearchCatalog(query, t);
assert.ok(hits.some((hit) => hit.entry.id === "sftp-transport-idle-ttl"), query);
}
}
});
test("settings search finds temporary network device mode", () => {
const tEnReal = (key: string) => en[key] ?? key;
const tZhTW = (key: string) => zhTW[key] ?? key;
-21
View File
@@ -671,13 +671,6 @@ export const SETTINGS_SEARCH_CATALOG: readonly SettingsSearchEntry[] = [
descriptionKey: "settings.sftp.autoSync.enableDesc",
sectionKey: "settings.sftp.autoSync",
},
{
id: "sftp-compressed-upload",
tab: "file-associations",
labelKey: "settings.sftp.compressedUpload.enable",
descriptionKey: "settings.sftp.compressedUpload.enableDesc",
sectionKey: "settings.sftp.compressedUpload",
},
{
id: "sftp-follow-terminal-cwd",
tab: "file-associations",
@@ -698,20 +691,6 @@ export const SETTINGS_SEARCH_CATALOG: readonly SettingsSearchEntry[] = [
labelKey: "settings.sftp.transferConcurrency",
descriptionKey: "settings.sftp.transferConcurrency.desc",
},
{
id: "sftp-skip-unchanged",
tab: "file-associations",
labelKey: "settings.sftp.skipUnchanged.enable",
descriptionKey: "settings.sftp.skipUnchanged.enableDesc",
sectionKey: "settings.sftp.transferConcurrency",
},
{
id: "sftp-transport-idle-ttl",
tab: "file-associations",
labelKey: "settings.ssh.transportIdleTtl",
descriptionKey: "settings.ssh.transportIdleTtl.desc",
sectionKey: "settings.sftp.transferConcurrency",
},
{
id: "sftp-default-opener",
tab: "file-associations",
@@ -281,26 +281,6 @@ test('wall deadline during authorization reports authorization failure rather th
assert.deepEqual(pty.writes, []);
});
test('idle Enter-PSSession remoting prompt allows the command to be sent', async () => {
const pty = new TestPty();
const session = { _promptTrackTail: '', lastIdlePrompt: '' };
const prompt = '[192.168.1.10]: PS C:\\Users\\Administrator\\Documents> ';
trackSessionIdlePrompt(session, `PowerShell 7\r\n${prompt}`);
assert.equal(readyPrompt(session), prompt);
const result = await startPtyJob(pty, 'Write-Output ok', {
shellKind: 'powershell',
getReadyPrompt: () => readyPrompt(session),
promptWaitTimeoutMs: 200,
timeoutMs: 1000,
typedInput: true,
}).resultPromise;
assert.equal(result.ok, true);
assert.equal(result.stdout, 'OK');
assert.equal(pty.writes.length, 1);
assert.match(pty.writes[0], /\$__NCMCP_/);
});
test('idle PowerShell whose raw tail was CSI-erased still receives the command', async (t) => {
t.mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const pty = new TestPty();
+3 -4
View File
@@ -50,12 +50,11 @@ function formatSyntheticEcho(command) {
}
// Default PowerShell prompt (e.g. `PS C:\Users\alice>`, `PS>`,
// `PS /home/alice>`), with optional Enter-PSSession / remoting `[host]: `
// and debug `[DBG]: ` prefixes. Anchored so command output that merely
// starts with `PS` (e.g. `PSO>`) doesn't match. The `\S` after `\s+` rejects literal
// `PS /home/alice>`). Anchored so command output that merely starts with
// `PS` (e.g. `PSO>`) doesn't match. The `\S` after `\s+` rejects literal
// `"PS >"` (which the default prompt never emits) so a script that prints
// such a line can't trick prompt-driven shell-kind selection.
const POWERSHELL_PROMPT_PATTERN = /^(?:\[[^\]\r\n]+\]: )*PS(?:\s+\S.*)?>$/;
const POWERSHELL_PROMPT_PATTERN = /^PS(?:\s+\S.*)?>$/;
// Default cmd.exe prompt (e.g. `C:\>`, `C:\Users\alice>`, `D:\data>`).
// Drive letter + optional path + `>`. Rejects `C: >` (space before `>`) and
-40
View File
@@ -112,46 +112,6 @@ test("isDefaultPowerShellPromptLine matches default shapes and rejects look-alik
assert.equal(isDefaultPowerShellPromptLine(null), false);
});
test("PowerShell prompt helpers recognize remoting and debug prefixes", () => {
for (const prompt of [
"[192.168.1.10]: PS C:\\Users\\Administrator\\Documents>",
"[SERVER01]: PS C:\\>",
"[host.example.com]: PS C:\\Windows\\System32>",
"[fe80::1]: PS C:\\>",
"[DBG]: PS C:\\Users\\alice>",
"[SERVER01]: [DBG]: PS C:\\>",
]) {
assert.equal(isDefaultPowerShellPromptLine(prompt), true, prompt);
assert.equal(extractTrailingIdlePrompt(`welcome\r\n${prompt}`), prompt, prompt);
}
});
test("PowerShell prompt helpers reject malformed remoting prefixes and look-alikes", () => {
for (const line of [
"[host] PS C:\\>",
"prefix [host]: PS C:\\>",
"[]: PS C:\\>",
"[ho]st]: PS C:\\>",
"[ho\rst]: PS C:\\>",
"[ho\nst]: PS C:\\>",
"[host]:PS C:\\>",
"[host]: PS >",
"PS >",
"PSO>",
"ZIPS>",
"",
null,
]) {
assert.equal(isDefaultPowerShellPromptLine(line), false, String(line));
assert.equal(extractTrailingIdlePrompt(line), "", String(line));
}
});
test("extractTrailingIdlePrompt preserves trailing space on a remoting prompt", () => {
const prompt = "[192.168.1.10]: PS C:\\Users\\Administrator\\Documents> ";
assert.equal(extractTrailingIdlePrompt(`welcome\r\n${prompt}`), prompt);
});
test("extracts a trailing cmd.exe idle prompt", () => {
// Windows OpenSSH default shell is cmd.exe; without capturing `C:\...>`
// AI exec cannot select the cmd wrapper when shellKind is still unset.