Module: Twin::Remote

Defined in:
lib/twin/remote.rb

Overview

Remote (ssh) targets, written exactly as rsync understands them: "user@host:/path" or "host:/path". A target counts as remote when a colon appears before the first slash. Sources stay local — twin pushes.

Constant Summary collapse

SSH_OPTS =
["-o", "BatchMode=yes", "-o", "ConnectTimeout=5"].freeze
STAT_SCRIPT =

Stat many paths in one ssh round-trip. Paths go over stdin (one per line), the remote loop answers "pathepoch" or "path-" for missing ones. Tries BSD stat first, then GNU — covers macOS and Linux. Returns => Time or nil-if-missing, or nil when ssh itself failed.

<<~SH.freeze
  while IFS= read -r p; do
    if [ -e "$p" ]; then
      printf '%s\t%s\n' "$p" "$(stat -f %m -- "$p" 2>/dev/null || stat -c %Y -- "$p")"
    else
      printf '%s\t-\n' "$p"
    fi
  done
SH

Class Method Summary collapse

Class Method Details

.mkdir_p(host, dir) ⇒ Object

Create a directory on the remote side (mkdir -p equivalent).



61
62
63
64
# File 'lib/twin/remote.rb', line 61

def mkdir_p(host, dir)
  _out, _err, status = Open3.capture3("ssh", *SSH_OPTS, host, "mkdir", "-p", shellesc(dir))
  status.success?
end

.reachable?(host) ⇒ Boolean

Non-interactive reachability probe (BatchMode: never asks for a password).

Returns:

  • (Boolean)


23
24
25
# File 'lib/twin/remote.rb', line 23

def reachable?(host)
  system("ssh", *SSH_OPTS, host, "true", out: File::NULL, err: File::NULL)
end

.remote?(target) ⇒ Boolean

Returns:

  • (Boolean)


12
13
14
# File 'lib/twin/remote.rb', line 12

def remote?(target)
  %r{\A[^/]+:}.match?(target.to_s)
end

.shellesc(s) ⇒ Object

Escape one argument for the remote shell (ssh joins args with spaces and hands the string to a shell — local exec-style arrays don't protect it).



68
69
70
# File 'lib/twin/remote.rb', line 68

def shellesc(s)
  "'" + s.gsub("'", "'\\\\''") + "'"
end

.split(target) ⇒ Object

"user@host:/path" → ["user@host", "/path"]



17
18
19
20
# File 'lib/twin/remote.rb', line 17

def split(target)
  host, path = target.to_s.split(":", 2)
  [host, path.to_s]
end

.stat_paths(host, paths) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/twin/remote.rb', line 41

def stat_paths(host, paths)
  return {} if paths.empty?
  out, _err, status = Open3.capture3(
    "ssh", *SSH_OPTS, host, STAT_SCRIPT,
    stdin_data: paths.join("\n") + "\n"
  )
  return nil unless status.success?

  result = {}
  out.each_line do |line|
    path, mtime = line.chomp.split("\t", 2)
    next unless path && mtime
    result[path] = mtime == "-" ? nil : Time.at(mtime.to_i)
  end
  result
rescue Errno::ENOENT
  nil # ssh not installed
end