#!/usr/bin/env python3
# sftpipe -- copy data from stdin to a remote file over SFTP
#
# sftpipe can be used to stream data to systems that don't support POSIX-style
# "cat > foo" remote commands (such as Windows, RouterOS, or OpenVMS).

import argparse
import paramiko
import re
import sys
import subprocess
import urllib.parse

def parse_dest(arg):
    if arg.startswith("sftp://"):
        u = urllib.parse.urlparse(arg)

        if not u.hostname:
            raise ValueError(f"URL {arg!r} has empty hostname")

        return u.username, u.hostname, u.path[1:]
    else:
        if m := re.match(r"^([^:]+):(.+)$", arg):
            host, path = m.groups()
        else:
            raise ValueError(f"could not parse {arg!r}")

        if m := re.match(r"^(.+)@([^@]+)$", host):
            user, host = m.groups()
        else:
            user = None

        return user, host, path

class OpenSshSubsystemChannel():
    """
    A socket-like object to be used in place of paramiko.channel.Channel(), in
    order to use Paramiko SFTP client with OpenSSH host/user authentication.
    """

    def __init__(self, endpoint, subsystem):
        self.ep = endpoint
        self.subsys = subsystem
        self.sshcmd = ["ssh", "-q", "-s", endpoint, subsystem]
        self.proc = subprocess.Popen(self.sshcmd,
                                     stdin=subprocess.PIPE,
                                     stdout=subprocess.PIPE)

    def get_name(self):
        return f"[fake channel to {self.subsys!r} on {self.ep!r}]"

    def send(self, buf):
        n = self.proc.stdin.write(buf)
        self.proc.stdin.flush()
        return n

    def recv(self, nbytes):
        return self.proc.stdout.read(nbytes)

    def close(self):
        self.proc.stdin.close()
        self.proc.wait()

parser = argparse.ArgumentParser()
parser.add_argument("-P", "--paramiko",
                    action="store_true",
                    help="use Paramiko as transport instead of OpenSSH")
parser.add_argument("-v", "--verbose",
                    action="store_true",
                    help="show connection progress")
parser.add_argument("dest",
                    metavar="HOST:PATH",
                    help="remote file (rcp-style path or sftp:// URL)")
args = parser.parse_args()

user, host, path = parse_dest(args.dest)

if args.verbose:
    print(f"sftpipe: Connecting to {host!r}...", file=sys.stderr)

# For most uses, OpenSSH is a more useful transport as it reuses the same
# configuration as `sftp` (and automatically deals with key passphrases,
# SSH host certificates, etc).
if args.paramiko:
    client = paramiko.client.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.client.WarningPolicy)
    client.connect(host,
                   username=user,
                   gss_kex=True,
                   gss_auth=True)
    sftp = client.open_sftp()
else:
    ep = f"{user}@{host}" if user else f"{host}"
    chan = OpenSshSubsystemChannel(ep, "sftp")
    sftp = paramiko.sftp_client.SFTPClient(chan)

if args.verbose:
    print(f"sftpipe: Uploading to {path!r}...", file=sys.stderr)

sftp.putfo(sys.stdin.buffer, path)
sftp.close()
