Martin Braun
Martin Braun

Reputation: 12589

How to transfer files via SSH using Deno?

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?

Upvotes: 2

Views: 812

Answers (2)

murphyslaw
murphyslaw

Reputation: 636

In Deno 2 the subprocess API has changed (see Migration Guide) which requires to replace Deno.run with new Deno.Command. Using @jsejcksn's answer answer, the updated Deno 2 version would be:

./scp.ts

const decoder = new TextDecoder();

export type ProcessOutput = {
  success: boolean;
  stderr: string;
  stdout: string;
};

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

  const result = await command.output();

  const success = result.success;
  const stderr = decoder.decode(result.stderr);
  const stdout = decoder.decode(result.stdout);

  return { success, 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): {cmd: string; args: string[]} {
  const isWindows = Deno.build.os === "windows";
  const processName = isWindows ? "pscp" : "scp";
  const platformArgs: string[] = [];

  // 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 { cmd: processName, args: platformArgs };
}

./main.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 { cmd, args } = createTransferArgs(options);
const { success, stderr, stdout } = await getProcessOutput(cmd, args);

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);

p.s.: I've slightly changed the ProcessOutput to not return a status object, but only the success variable.

Upvotes: 0

jsejcksn
jsejcksn

Reputation: 33749

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/[email protected]/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);

Upvotes: 3

Related Questions