Module: Twin::Sync
- Defined in:
- lib/twin/sync.rb
Constant Summary collapse
- ITEMIZE_CHANGE =
A line from ‘rsync –itemize-changes` describing a real change: an itemize code whose first column is the update type (< > c h *) and second the file type (f d L D S), e.g. “>f+++++++++”, “cd+++++++++”, “*deleting”. No-op runs emit no such line; headers (“sending …”, “created directory …”) and the summary don’t match. Deterministic — no scraping of prose.
/\A[<>ch*][fdLDS]/
Class Method Summary collapse
-
.mounted?(path) ⇒ Boolean
True if the path lives on a mounted volume other than the root filesystem.
-
.render_job(cfg, job, dry_run: false) ⇒ Object
Render a template Job: read source, substitute {vars}, write if changed.
- .run(args) ⇒ Object
-
.run_job(cfg, job, dry_run: false) ⇒ Object
Sync one Job.
-
.run_program(cfg, program, dry_run: false) ⇒ Object
Sync all jobs in a Program.
-
.transferred?(output) ⇒ Boolean
True when rsync reported at least one changed item.
Class Method Details
.mounted?(path) ⇒ Boolean
True if the path lives on a mounted volume other than the root filesystem. Walks up parents until it finds a mount point (different device than parent) or hits “/” (path is on the root volume, not externally mounted).
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/twin/sync.rb', line 22 def mounted?(path) return false unless File.exist?(path) current = File.(path) until current == "/" parent = File.("..", current) return true if File.stat(current).dev != File.stat(parent).dev current = parent end false rescue Errno::ENOENT false end |
.render_job(cfg, job, dry_run: false) ⇒ Object
Render a template Job: read source, substitute {vars}, write if changed. Returns [success, output, changed].
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/twin/sync.rb', line 37 def render_job(cfg, job, dry_run: false) src = job.source_path tgt = job.target_path return [false, "source not found: #{src}", false] unless File.exist?(src) return [false, "render: source must be a file, not a directory: #{src}", false] if File.directory?(src) begin rendered = Twin::Template.render_file(src, cfg.var_map, context: job.path) rescue => e return [false, e., false] end if dry_run return [true, "(dry-run) would render #{File.basename(src)} → #{tgt}", false] end FileUtils.mkdir_p(File.dirname(tgt)) current = File.exist?(tgt) ? File.binread(tgt) : nil changed = (current != rendered) if changed File.binwrite(tgt, rendered) output = "rendered #{File.basename(src)} → #{tgt}" else output = "#{File.basename(src)}: content unchanged" end if !job.cmd.empty? if changed cmd_out, cmd_status = run(["sh", "-c", job.cmd]) output += "\ncmd: #{job.cmd}\n#{cmd_out}" unless cmd_status.success? output += "cmd failed (exit #{cmd_status.exitstatus})" return [false, output, changed] end else output += "\ncmd skipped (content unchanged)" end end [true, output, changed] end |
.run(args) ⇒ Object
136 137 138 139 140 141 142 |
# File 'lib/twin/sync.rb', line 136 def run(args) require "open3" stdout, stderr, status = Open3.capture3(*args) [stdout + stderr, status] rescue Errno::ENOENT raise "command not found: #{args.first}" end |
.run_job(cfg, job, dry_run: false) ⇒ Object
Sync one Job. Returns [success, combined_output, transferred]. transferred is true when rsync actually moved bytes (false on no-op or dry_run).
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/twin/sync.rb', line 84 def run_job(cfg, job, dry_run: false) return render_job(cfg, job, dry_run: dry_run) if job.render src = job.source_path tgt = job.target_path return [false, "source not found: #{src}", false] unless File.exist?(src) FileUtils.mkdir_p(File.dirname(tgt)) args = ["rsync", "-av", "--itemize-changes", "--update"] args << "--delete" if job.delete args << "--dry-run" if dry_run cfg.global_excludes.each { |ex| args << "--exclude=#{ex}" } job.excludes.each { |ex| args << "--exclude=#{ex}" } if File.directory?(src) args << "#{src}/" << "#{tgt}/" else args << src << tgt end output, status = run(args) return [false, output, false] unless status.success? xfr = !dry_run && transferred?(output) if job.conflict && !xfr && !dry_run output += "\nskipped: target is newer, source not synced" end if !job.cmd.empty? && !dry_run if xfr cmd_out, cmd_status = run(["sh", "-c", job.cmd]) output += "\ncmd: #{job.cmd}\n#{cmd_out}" unless cmd_status.success? output += "cmd failed (exit #{cmd_status.exitstatus})" return [false, output, xfr] end else output += "\ncmd skipped (nothing transferred)" end end [true, output, xfr] end |
.run_program(cfg, program, dry_run: false) ⇒ Object
Sync all jobs in a Program. Returns array of [job, success, output].
132 133 134 |
# File 'lib/twin/sync.rb', line 132 def run_program(cfg, program, dry_run: false) program.active_jobs.map { |job| [job, *run_job(cfg, job, dry_run: dry_run)] } end |
.transferred?(output) ⇒ Boolean
True when rsync reported at least one changed item.
15 16 17 |
# File 'lib/twin/sync.rb', line 15 def transferred?(output) output.lines.any? { |l| ITEMIZE_CHANGE.match?(l) } end |