How to transfer files via SSH using Deno?

Viewed 161

I'm looking for a way to transfer files via SSH using Deno. I'm not trying to allow the user to upload files through a website, instead I want to use Deno as a scripting language to upload files to a server similarly to scp or pscp. Unfortunately, neither of those have been used in any Deno wrapper, so I wonder what the best fastest solution would be if I want maintain cross-compatibility?

1 Answers

Creating a wrapper is simpler than you might think: you can use the subprocess API to create calls to scp or pscp, and you can discriminate platform environment using Deno.build.os. Combining them to achieve your goal is straightforward:

./scp.ts:

const decoder = new TextDecoder();

export type ProcessOutput = {
  status: Deno.ProcessStatus;
  stderr: string;
  stdout: string;
};

/**
 * Convenience wrapper around subprocess API.
 * Requires permission `--allow-run`.
 */
export async function getProcessOutput(cmd: string[]): Promise<ProcessOutput> {
  const process = Deno.run({ cmd, stderr: "piped", stdout: "piped" });

  const [status, stderr, stdout] = await Promise.all([
    process.status(),
    decoder.decode(await process.stderrOutput()),
    decoder.decode(await process.output()),
  ]);

  process.close();
  return { status, stderr, stdout };
}

// Add any config options you want to use here
// (e.g. maybe a config instead of username/host)
// The point is that you decide the API:
export type TransferOptions = {
  sourcePath: string;
  host: string;
  username: string;
  destPath: string;
};

export function createTransferArgs(options: TransferOptions): string[] {
  const isWindows = Deno.build.os === "windows";
  const processName = isWindows ? "pscp" : "scp";
  const platformArgs: string[] = [processName];

  // Construct your process args here using your options,
  // handling any platform variations:
  if (isWindows) {
    // Translate to pscp args here...
  } else {
    // Translate to scp args here...
    // example:
    platformArgs.push(options.sourcePath);
    platformArgs.push(
      `${options.username}@${options.host}:${options.destPath}`,
    );
  }

  return platformArgs;
}

./main.ts:

import * as path from "https://deno.land/std@0.129.0/path/mod.ts";

import {
  createTransferArgs,
  getProcessOutput,
  type TransferOptions,
} from "./scp.ts";

// locally (relative to CWD): ./data/example.json (or on Windows: .\data\example.json)
const fileName = "example.json";
const sourcePath = path.join(Deno.cwd(), "data", fileName);
// on remote (uses *nix FS paths): /repo/example.json
const destPath = path.posix.join("/", "repo", fileName);

const options: TransferOptions = {
  sourcePath,
  host: "server.local",
  username: "user1",
  destPath,
};

const transferArgs = createTransferArgs(options);
const { status: { success }, stderr, stdout } = await getProcessOutput(
  transferArgs,
);

if (!success) {
  // something went wrong, do something with stderr if you want
  console.error(stderr);
  Deno.exit(1);
}

// else continue...
console.log(stdout);
Deno.exit(0);

Related