ruby-livesync 1.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7413a1c3e43254fbaa87386c88f0a550ec6e9f7a17331e74daa182c3aee7ee67
4
+ data.tar.gz: 34320b16a5886835146b9eb89a408e3481b9afdd62eafbd450ec2b83767aa8a9
5
+ SHA512:
6
+ metadata.gz: 604aec4e0bfa4cf29cfbfec5837672d36289e98af3a537cf6dc4ad11706110a2265f079ace49820926eaf5358160caae85b6042ae15b855fa3af4fd4cd0ef012
7
+ data.tar.gz: fc87708fba1a5db5c1a88eb81bca51268a80bcf2d30bfe9ddf80d7df9656ba3c2c28de7558e8abe375809a8432ec819de5afbe88e6a7295a19a3b0d7b1067696
data/bin/livesync ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'ruby-livesync'
6
+
7
+ config = ARGV[0]
8
+ LiveSync::Daemon.start config
9
+
@@ -0,0 +1,49 @@
1
+ module LiveSync
2
+ class Daemon
3
+
4
+ def self.start config
5
+ new(config).start
6
+ end
7
+
8
+ attr_reader :config
9
+
10
+ def initialize config
11
+ @config = config
12
+ @pids = []
13
+ @syncs = Hash.new{ |h, k| h[k] = [] }
14
+ end
15
+
16
+ def start
17
+ Process.setpgrp
18
+ Process.setproctitle 'livesync'
19
+ instance_eval File.read(config), config
20
+ run
21
+ Process.waitall
22
+ end
23
+
24
+ def sync name_or_path, &block
25
+ s = Sync.new name_or_path
26
+ s.dsl_apply(&block)
27
+ @syncs[s.user] << s
28
+ end
29
+
30
+ protected
31
+
32
+ def run
33
+ @syncs.each do |user, syncs|
34
+ User.wrap user do
35
+ syncs.each do |s|
36
+ s.run
37
+ s.guard
38
+ rescue => e
39
+ msg = e.message
40
+ msg += "\n#{e.backtrace.join "\n"}" if Log.debug
41
+ Log.fatal msg
42
+ end
43
+ Process.waitall
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ module LiveSync
2
+ module DSL
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+
8
+ class_attribute :ctx
9
+
10
+ class_attribute :attrs
11
+ self.attrs = []
12
+
13
+ def self.dsl attr, default: nil, type: nil, &block
14
+ self.attrs << attr
15
+ define_method attr do |value=nil|
16
+ return instance_variable_get("@#{attr}") || default unless value
17
+ raise "#{ctx}: incorrect type for `#{attr}`" if type and !value.is_a? type
18
+ instance_exec value, &block if block
19
+ instance_variable_set "@#{attr}", value
20
+ end
21
+ end
22
+
23
+ def dsl_apply &block
24
+ if b = binding and bs = block.source.match(/do(.+)end$/m)&.captures&.first
25
+ b.eval bs
26
+ attrs.each do |a| # read local variables
27
+ next unless a.in? b.local_variables
28
+ send a, b.local_variable_get(a)
29
+ end
30
+ else
31
+ instance_exec &block
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ module LiveSync
2
+ class Log
3
+
4
+ class_attribute :ctx
5
+ class_attribute :global
6
+ self.global = self.new
7
+ class << self
8
+ delegate_missing_to :global
9
+ end
10
+
11
+ class_attribute :debug
12
+ self.debug = !!ENV['DEBUG']
13
+
14
+ def initialize ctx=nil
15
+ self.ctx = ctx
16
+ end
17
+
18
+ def debug msg
19
+ return unless debug
20
+ puts "DEBUG: #{parse msg}"
21
+ end
22
+
23
+ def info msg
24
+ puts "INFO: #{parse msg}"
25
+ end
26
+
27
+ def warning msg
28
+ STDERR.puts "WARNING: #{parse msg}"
29
+ end
30
+
31
+ def error msg
32
+ STDERR.puts "ERROR: #{parse msg}"
33
+ end
34
+
35
+ def fatal msg
36
+ STDERR.puts "FATAL: #{parse msg}"
37
+ end
38
+
39
+ def parse msg
40
+ msg = "#{ctx}: #{msg}" if ctx
41
+ msg
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module LiveSync
2
+ class Rsync
3
+
4
+ attr_reader :sync, :opts
5
+
6
+ def initialize sync
7
+ @sync = sync
8
+ @opts = '-ax --partial'
9
+ end
10
+
11
+ def running?
12
+ @wait_thr
13
+ end
14
+
15
+ def initial
16
+ run :initial
17
+ end
18
+
19
+ def from_list paths
20
+ run :partial, '--files-from=-' do |stdin, stdout, stderr|
21
+ stdin.write paths.join "\n"
22
+ stdin.close
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def run type, args=nil
29
+ cmd = "rsync -e '#{rsh}' #{opts} #{sync.source} #{sync.target} #{args}"
30
+ sync.log.info "#{type}: starting with cmd: #{cmd}"
31
+ stdin, stdout, stderr, @wait_thr = Open3.popen3 cmd
32
+ yield stdin, stdout, stderr if block_given?
33
+ Thread.new do
34
+ Process.wait @wait_thr.pid rescue Errno::ECHILD; nil
35
+ @wait_thr = nil
36
+ sync.log.info "#{type}: finished"
37
+ end
38
+ end
39
+
40
+ def rsh
41
+ "ssh -o ControlPath=#{sync.ssh.cpath}"
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ require 'tempfile'
2
+ require 'open3'
3
+
4
+ module LiveSync
5
+ class Ssh
6
+
7
+ class_attribute :cache
8
+ self.cache = {}
9
+
10
+ def self.connect userhost
11
+ cache[userhost] ||= new userhost
12
+ end
13
+
14
+ attr_reader :userhost, :cpath
15
+
16
+ def initialize userhost
17
+ @userhost = userhost
18
+ @tmpfile = Tempfile.new "livesync-controlpath"
19
+ @cpath = @tmpfile.path
20
+ File.unlink @cpath
21
+ connect
22
+ end
23
+
24
+ def connect
25
+ Thread.new do
26
+ loop do
27
+ cmd = "ssh -nN -o ControlMaster=yes -o ControlPath=#{cpath} #{userhost}"
28
+ stdin, stdout, stderr, @wait_thr = Open3.popen3 cmd
29
+ Process.wait @wait_thr.pid
30
+ Log.error "ssh/#{userhost}: #{stderr.read}"
31
+ end
32
+ end
33
+ sleep 1 while !available?
34
+ end
35
+
36
+ def available?
37
+ return false unless File.exist? cpath
38
+ system "ssh -O check -o ControlPath=#{cpath} dummy > /dev/null 2>&1"
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,80 @@
1
+ module LiveSync
2
+ class Sync
3
+
4
+ include DSL
5
+
6
+ attr_reader :name
7
+ attr_reader :scheduler
8
+ attr_reader :log
9
+ attr_reader :watcher
10
+ attr_reader :ssh
11
+ attr_reader :rsync
12
+
13
+ def initialize name = nil
14
+ fill_name name
15
+ source name if File.exist? name
16
+ @to_sync = Set.new
17
+ end
18
+
19
+ def fill_name name
20
+ return if @name
21
+ self.ctx = @name = name
22
+ @log = Log.new ctx
23
+ end
24
+
25
+ dsl :user, default: :root
26
+ dsl :source do |source|
27
+ raise "#{ctx}: source not found" unless File.exist? source
28
+ fill_name source
29
+ @pathname = Pathname.new source
30
+ end
31
+
32
+ dsl :target do |target|
33
+ @userhost, @target_path = target.split(':')
34
+ raise "#{ctx}: missing target path" unless @target_path
35
+ @user, @host = if @userhost.index '@' then @userhost.split('@') else [@user, @userhost] end
36
+ end
37
+
38
+ dsl :delay, default: 5, type: Integer
39
+
40
+ def run
41
+ raise "#{ctx}: missing target" unless @target
42
+ @ssh = Ssh.connect @userhost
43
+ @rsync = Rsync.new self
44
+ end
45
+
46
+ def guard
47
+ fork do
48
+ Process.setproctitle "livesync: sync #{ctx}"
49
+ @watcher = Watcher.new
50
+ @scheduler = Rufus::Scheduler.new
51
+
52
+ @watcher.dir_rwatch source, *%i[create modify], &method(:track)
53
+ @rsync.initial
54
+ schedule
55
+ sleep 1.day while true
56
+ end
57
+ end
58
+
59
+ def track event
60
+ @to_sync << Pathname.new(event.absolute_name).relative_path_from(@pathname).to_s
61
+ end
62
+
63
+ def schedule
64
+ @scheduler.in "#{delay}s", &method(:check)
65
+ end
66
+
67
+ def check
68
+ return if @rsync.running?
69
+ @watcher.process # calls #track
70
+ return if @to_sync.blank?
71
+ @rsync.from_list @to_sync
72
+ @to_sync.clear
73
+ ensure
74
+ schedule
75
+ end
76
+
77
+ protected
78
+
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ module LiveSync
2
+ class User
3
+
4
+ def self.wrap user
5
+ fork do
6
+ Process.setproctitle "livesync: user #{user}"
7
+ Process.uid = Process::UID.from_name user.to_s
8
+ yield
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module LiveSync
2
+
3
+ VERSION = '1.0.0.beta1'
4
+
5
+ end
@@ -0,0 +1,37 @@
1
+ module LiveSync
2
+ class Watcher
3
+
4
+ attr_reader :notifier
5
+ delegate_missing_to :notifier
6
+
7
+ def initialize
8
+ @notifier = INotify::Notifier.new
9
+ end
10
+
11
+ def watch path, *modes
12
+ Log.info "#{path}: watching for #{modes.join ','}"
13
+ modes = %i[all_events] if modes.blank?
14
+
15
+ notifier.watch path, *modes do |event|
16
+ yield event
17
+ end
18
+ self
19
+ end
20
+
21
+ def dir_rwatch path, *modes
22
+ raise "#{path}: not a directory" unless File.directory? path
23
+ modes = %i[create modify] if modes.blank?
24
+
25
+ tgts = [path] + Dir.glob("#{path}/**/*/")
26
+ tgts.each do |t|
27
+ notifier.watch t, *modes do |event|
28
+ yield event
29
+ end
30
+ rescue => e
31
+ Log.warning "#{t}: skipping due to #{e.class}: #{e.message}"
32
+ end
33
+ self
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module LiveSync
2
+
3
+ end
4
+
5
+ require_relative 'live_sync/version'
6
+ require_relative 'live_sync/dsl'
7
+ require_relative 'live_sync/log'
8
+ require_relative 'live_sync/user'
9
+ require_relative 'live_sync/ssh'
10
+ require_relative 'live_sync/rsync'
11
+ require_relative 'live_sync/watcher'
12
+ require_relative 'live_sync/sync'
13
+ require_relative 'live_sync/daemon'
14
+
@@ -0,0 +1,8 @@
1
+ require 'pry'
2
+ require 'active_support/all'
3
+
4
+ require 'rb-inotify'
5
+ require 'rufus-scheduler'
6
+
7
+ require_relative 'ruby-livesync/live_sync'
8
+
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-livesync
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.beta1
5
+ platform: ruby
6
+ authors:
7
+ - Braulio Oliveira
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rb-inotify
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rufus-scheduler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - brauliobo@gmail.com
72
+ executables:
73
+ - livesync
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - bin/livesync
78
+ - lib/ruby-livesync.rb
79
+ - lib/ruby-livesync/live_sync.rb
80
+ - lib/ruby-livesync/live_sync/daemon.rb
81
+ - lib/ruby-livesync/live_sync/dsl.rb
82
+ - lib/ruby-livesync/live_sync/log.rb
83
+ - lib/ruby-livesync/live_sync/rsync.rb
84
+ - lib/ruby-livesync/live_sync/ssh.rb
85
+ - lib/ruby-livesync/live_sync/sync.rb
86
+ - lib/ruby-livesync/live_sync/user.rb
87
+ - lib/ruby-livesync/live_sync/version.rb
88
+ - lib/ruby-livesync/live_sync/watcher.rb
89
+ homepage: https://github.com/brauliobo/ruby-livesync
90
+ licenses:
91
+ - GPLv3
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">"
105
+ - !ruby/object:Gem::Version
106
+ version: 1.3.1
107
+ requirements: []
108
+ rubygems_version: 3.3.25
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Lightweight and fast live sync daemon
112
+ test_files: []