NAME

App::FileSync::Transport::SSH - run the server over ssh

SYNOPSIS

my $transport = App::FileSync::Transport::SSH->new(
	server => 'filesync@files.example.com', repo => 'docs', client => 'laptop',
	identity => '~/.ssh/filesync_ed25519',
);
...
$transport->close or warn $transport->error_text;

METHODS

new

Purpose: start filesync-server on the server host for one session.

Args: server (user@host, host or user@[v6]), repo, client, optional role (default client), server_command (default filesync-server), host_key_checking (yes, the default, or accept-new), known_hosts, identity, port, command (the local ssh program), extra_args (arrayref of extra ssh arguments).

Returns: the transport.

Side Effects: runs ssh ... SERVER PROGRAM --repo R --client C --role X --proto 2. ssh's standard error goes to a temporary file, never the protocol pipe, and is available from "error_text". On Windows a .cmd or .bat ssh wrapper is started through COMSPEC, since CreateProcess cannot start one directly.

Usage:

my $transport = App::FileSync::Transport::SSH->new(%args);

EXAMPLE

my $t = App::FileSync::Transport::SSH->new(
	server => 'backup', repo => 'photos', client => 'nas', role => 'replica', port => 2222,
);
print join(' ', @{ $t->command }), "\n";

API SPECIFICATION

INPUT

{
	server            => { type => 'string', matches => $SERVER },
	repo              => { type => 'string', min => 1, max => 64 },
	client            => { type => 'string', min => 1, max => 64 },
	role              => { type => 'string', optional => 1, memberof => [ 'client', 'replica' ] },
	server_command    => { type => 'string', optional => 1, matches => $PROGRAM },
	host_key_checking => { type => 'string', optional => 1, memberof => [ 'yes', 'accept-new' ] },
	known_hosts       => { type => 'string', optional => 1 },
	identity          => { type => 'string', optional => 1 },
	port              => { type => 'integer', optional => 1, min => 1, max => 65535 },
	command           => { type => 'string', optional => 1 },
	extra_args        => { type => 'arrayref', optional => 1 },
}

OUTPUT

{ type => 'object', isa => 'App::FileSync::Transport::SSH' }

MESSAGES

Key                  Meaning                                    Resolution
transport.missing    server, repo or client absent              fix the repo config
transport.bad_value  a value is unsafe or out of range          fix the repo config or settings
transport.bad_id     the repo or client is not a plain id       fix the repo config
io.write             the error file cannot be created           check the temp directory

FORMAL SPECIFICATION

pre server ∈ SERVER ∧ repo ∈ Id ∧ client ∈ Id ∧ role ∈ ROLES ∧ program ∈ PROGRAM
    ∧ (port = ⊥ ∨ 0 < port ≤ 65535) ∧ ∀ a : extra_args • a ∈ TEXT ∧ '\n' ¬in; a
argv = ⟨ssh⟩ ⁀ options ⁀ ⟨server, program, --repo, repo, --client, client, --role, role, --proto, PROTO⟩

PSEUDOCODE

validate every argument
build ssh's options: fixed ones, host key policy, known hosts, identity,
port, extra arguments
append the server, then the remote command
wrap a .cmd/.bat ssh in COMSPEC on Windows
start it with standard error going to a temporary file

reader

Documented with "exit_status" below.

writer

Documented with "exit_status" below.

command

Documented with "exit_status" below.

exit_status

Purpose: the session's pipes, the argv that was run, and ssh's exit status.

Args: none.

Returns: the read and write handles; a copy of the argv (for --verbose and tests); the exit status after "close" (undef before).

Side Effects: none.

Usage:

my $argv = $transport->command;

EXAMPLE

print "ran: @{ $transport->command }\n";

API SPECIFICATION

INPUT

[ { name => 'self', type => 'object' } ]

OUTPUT

{ type => 'scalar' }

MESSAGES

None.

FORMAL SPECIFICATION

command = copy(self.command) ; exit_status = self.status

close

Purpose: end the ssh session.

Args: none.

Returns: 1 when ssh exited 0, else 0. Repeated calls return 1.

Side Effects: closes both pipes, reaps ssh, records its exit status and signal, and reads what it wrote to standard error.

Usage:

$transport->close or warn $transport->error_text, "\n";

EXAMPLE

unless ($transport->close) {
	printf "ssh exited %d: %s\n", $transport->exit_status, $transport->error_text;
}

API SPECIFICATION

INPUT

[ { name => 'self', type => 'object' } ]

OUTPUT

{ type => 'integer', memberof => [ 0, 1 ] }

MESSAGES

None.

FORMAL SPECIFICATION

close = 1 ⇔ closed ∨ exit(ssh) = 0

error_text

Purpose: what ssh or the remote shell said, for when the protocol failed.

Args: none.

Returns: the text, trimmed, with blank lines collapsed.

Side Effects: reads the error file if "close" has not yet.

Usage:

my $why = $transport->error_text;

EXAMPLE

print $transport->error_text, "\n";	# "Permission denied (publickey)."

API SPECIFICATION

INPUT

[ { name => 'self', type => 'object' } ]

OUTPUT

{ type => 'string' }

MESSAGES

None.

FORMAL SPECIFICATION

error_text = squeeze(trim(stderr))

valid_server

Documented with "valid_program" below.

valid_program

Purpose: check a server address and a remote program path.

Args: the value.

Returns: 1 or 0.

A server is user@host, host or user@[ipv6]; the user and host must start with a letter or digit, so neither can be read as an ssh option (a leading -) and nothing can split into another word. A program is a path of letters, digits, ., _, / and -, not starting with -.

Side Effects: none.

Usage:

die "bad server\n" unless App::FileSync::Transport::SSH::valid_server($server);

EXAMPLE

App::FileSync::Transport::SSH::valid_server('filesync@files.example.com');	# 1
App::FileSync::Transport::SSH::valid_server('-oProxyCommand=sh');		# 0
App::FileSync::Transport::SSH::valid_server('-v@host');				# 0

API SPECIFICATION

INPUT

[ { name => 'value', type => 'string', optional => 1 } ]

OUTPUT

{ type => 'integer', memberof => [ 0, 1 ] }

MESSAGES

None.

FORMAL SPECIFICATION

valid_server(v) ⇔ v ∈ SERVER
valid_program(v) ⇔ v ∈ PROGRAM

LIMITATIONS

  • extra_args are passed to ssh unchecked apart from embedded NULs and newlines: they are the user's own options, and can therefore do anything ssh options can (-o ProxyCommand=... included). Only trusted configuration should set them.

  • known_hosts and identity are not tilde-expanded here; ssh expands ~ in -i but not in UserKnownHostsFile on every version.

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

LICENCE AND COPYRIGHT

Copyright 2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.