sshdeno

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?


Solution

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