This commit is contained in:
@@ -60,6 +60,8 @@ interface SshFileTransferDownloadResult {
|
||||
local: SshFileTransferStat;
|
||||
strategy: "tcp-pool-stdout-stream";
|
||||
transport: "host.ssh.tcp-pool";
|
||||
sourceRoute: string;
|
||||
sourcePath: string;
|
||||
chunks: number;
|
||||
elapsedMs: number;
|
||||
throughputBytesPerSecond: number;
|
||||
@@ -76,6 +78,8 @@ export interface SshVerifiedDownloadResult {
|
||||
transfer: {
|
||||
strategy: SshFileTransferDownloadResult["strategy"];
|
||||
transport: SshFileTransferDownloadResult["transport"];
|
||||
sourceRoute: string;
|
||||
sourcePath: string;
|
||||
chunks: number;
|
||||
elapsedMs: number;
|
||||
throughputBytesPerSecond: number;
|
||||
@@ -174,6 +178,8 @@ export async function downloadSshFileVerified(
|
||||
transfer: {
|
||||
strategy: read.strategy,
|
||||
transport: read.transport,
|
||||
sourceRoute: read.sourceRoute,
|
||||
sourcePath: read.sourcePath,
|
||||
chunks: read.chunks,
|
||||
elapsedMs: read.elapsedMs,
|
||||
throughputBytesPerSecond: read.throughputBytesPerSecond,
|
||||
@@ -293,19 +299,24 @@ async function downloadRemoteFileVerified(
|
||||
blocker: "streamRemoteCommand unavailable",
|
||||
});
|
||||
}
|
||||
const streamSource = invocation.route.plane === "win"
|
||||
? windowsDownloadStreamSource(invocation.route, remotePath)
|
||||
: { route: invocation.route, path: remotePath };
|
||||
if (invocation.route.plane === "win") {
|
||||
throw new SshFileTransferError("ssh download requires tcp-pool stdout streaming; win routes are not supported by the safe binary stream path", {
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
event: "unidesk.ssh.download.win-route-mapped",
|
||||
at: new Date().toISOString(),
|
||||
route: invocation.route.raw,
|
||||
providerId: invocation.providerId,
|
||||
remotePath,
|
||||
localPath,
|
||||
requiredTransport: "host.ssh.tcp-pool",
|
||||
blocker: "win route has no verified binary stdout stream path",
|
||||
});
|
||||
sourceRoute: streamSource.route.raw,
|
||||
sourcePath: streamSource.path,
|
||||
transport: "host.ssh.tcp-pool",
|
||||
})}\n`);
|
||||
}
|
||||
const remote = await statRemoteFile(invocation, executor, builders, remotePath);
|
||||
await mkdir(path.dirname(localPath), { recursive: true });
|
||||
const remoteCommand = buildStreamDownloadRemoteCommand(invocation.route, builders, remotePath);
|
||||
const remoteCommand = buildStreamDownloadRemoteCommand(streamSource.route, builders, streamSource.path);
|
||||
const startedAtMs = Date.now();
|
||||
const hash = createHash("sha256");
|
||||
const output = createWriteStream(localPath, { flags: "w", mode: 0o600 });
|
||||
@@ -343,6 +354,8 @@ async function downloadRemoteFileVerified(
|
||||
local,
|
||||
strategy: "tcp-pool-stdout-stream",
|
||||
transport: "host.ssh.tcp-pool",
|
||||
sourceRoute: streamSource.route.raw,
|
||||
sourcePath: streamSource.path,
|
||||
chunks: chunkCount,
|
||||
elapsedMs,
|
||||
throughputBytesPerSecond: Math.round((actualBytes * 1000) / elapsedMs),
|
||||
@@ -355,6 +368,94 @@ async function downloadRemoteFileVerified(
|
||||
}
|
||||
}
|
||||
|
||||
function windowsDownloadStreamSource(route: ParsedSshRoute, remotePath: string): { route: ParsedSshRoute; path: string } {
|
||||
const mappedPath = windowsPathToWslMountPath(remotePath, route.workspace);
|
||||
return {
|
||||
route: {
|
||||
providerId: route.providerId,
|
||||
plane: "host",
|
||||
entry: null,
|
||||
namespace: null,
|
||||
resource: null,
|
||||
container: null,
|
||||
workspace: null,
|
||||
raw: `${route.providerId}:${mappedPath}`,
|
||||
},
|
||||
path: mappedPath,
|
||||
};
|
||||
}
|
||||
|
||||
function windowsPathToWslMountPath(rawPath: string, basePath: string | null): string {
|
||||
const absolute = resolveWindowsTransferPath(rawPath, basePath);
|
||||
const match = absolute.match(/^([A-Za-z]):\\(.*)$/u);
|
||||
if (match === null) {
|
||||
throw new SshFileTransferError("ssh win download can only auto-map drive-letter paths to WSL /mnt/<drive>", {
|
||||
remotePath: rawPath,
|
||||
basePath,
|
||||
supported: "D:\\path\\file or route workspace D518:win/d/path plus relative file",
|
||||
unsupported: "UNC paths and paths without a drive letter",
|
||||
fallback: "copy the file to a drive-mounted path, then retry the same download command",
|
||||
});
|
||||
}
|
||||
const drive = match[1]!.toLowerCase();
|
||||
const rest = match[2] ?? "";
|
||||
const segments = normalizeWindowsPathSegments(rest.split(/[\\/]+/u));
|
||||
return `/mnt/${drive}${segments.length === 0 ? "" : `/${segments.join("/")}`}`;
|
||||
}
|
||||
|
||||
function resolveWindowsTransferPath(rawPath: string, basePath: string | null): string {
|
||||
const normalizedRaw = stripWindowsLongPathPrefix(rawPath.trim()).replace(/\//gu, "\\");
|
||||
if (normalizedRaw.length === 0) {
|
||||
throw new SshFileTransferError("ssh win download requires a non-empty remote path");
|
||||
}
|
||||
if (/^\\\\/u.test(normalizedRaw)) {
|
||||
throw new SshFileTransferError("ssh win download cannot auto-map UNC paths to WSL /mnt/<drive>", {
|
||||
remotePath: rawPath,
|
||||
fallback: "copy the file to a drive-letter path such as D:\\tmp, then retry download",
|
||||
});
|
||||
}
|
||||
if (/^[A-Za-z]:/u.test(normalizedRaw)) return normalizeWindowsAbsolutePath(normalizedRaw);
|
||||
const base = basePath === null ? "" : stripWindowsLongPathPrefix(basePath.trim()).replace(/\//gu, "\\");
|
||||
const baseMatch = base.match(/^([A-Za-z]):\\?(.*)$/u);
|
||||
if (baseMatch === null) {
|
||||
throw new SshFileTransferError("ssh win download with a relative path requires a drive-letter route workspace", {
|
||||
remotePath: rawPath,
|
||||
routeWorkspace: basePath,
|
||||
example: "trans D518:win/d/tmp download image.png /tmp/image.png",
|
||||
});
|
||||
}
|
||||
const drive = baseMatch[1]!;
|
||||
const baseRest = baseMatch[2] ?? "";
|
||||
return normalizeWindowsAbsolutePath(`${drive}:\\${[baseRest, normalizedRaw].filter((part) => part.length > 0).join("\\")}`);
|
||||
}
|
||||
|
||||
function stripWindowsLongPathPrefix(value: string): string {
|
||||
if (value.startsWith("\\\\?\\")) return value.slice(4);
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeWindowsAbsolutePath(value: string): string {
|
||||
const match = value.match(/^([A-Za-z]):\\?(.*)$/u);
|
||||
if (match === null) return value;
|
||||
const drive = match[1]!.toUpperCase();
|
||||
const segments = normalizeWindowsPathSegments((match[2] ?? "").split(/[\\/]+/u));
|
||||
return `${drive}:\\${segments.join("\\")}`;
|
||||
}
|
||||
|
||||
function normalizeWindowsPathSegments(segments: string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
for (const segment of segments) {
|
||||
if (segment.length === 0 || segment === ".") continue;
|
||||
if (segment === "..") {
|
||||
if (normalized.length === 0) throw new SshFileTransferError("ssh win download path escapes the drive root");
|
||||
normalized.pop();
|
||||
continue;
|
||||
}
|
||||
normalized.push(segment);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function emitDownloadProgress(
|
||||
invocation: ParsedSshInvocation,
|
||||
remotePath: string,
|
||||
|
||||
Reference in New Issue
Block a user