MrMurano 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/Gemfile +9 -0
- data/MrMurano.gemspec +35 -0
- data/README.markdown +27 -0
- data/Rakefile +25 -0
- data/TODO.taskpaper +45 -0
- data/bin/mr +66 -0
- data/lib/MrMurano/Account.rb +176 -0
- data/lib/MrMurano/Solution-Endpoint.rb +107 -0
- data/lib/MrMurano/Solution-File.rb +137 -0
- data/lib/MrMurano/Solution-ServiceConfig.rb +86 -0
- data/lib/MrMurano/Solution-Services.rb +163 -0
- data/lib/MrMurano/Solution-Users.rb +123 -0
- data/lib/MrMurano/Solution.rb +318 -0
- data/lib/MrMurano/configFile.rb +194 -0
- data/lib/MrMurano/hash.rb +18 -0
- data/lib/MrMurano/shelledCommand.rb +43 -0
- data/lib/MrMurano/status.rb +106 -0
- data/lib/MrMurano/sync.rb +132 -0
- data/lib/MrMurano/version.rb +4 -0
- data/lib/MrMurano.rb +13 -0
- metadata +166 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'terminal-table'
|
2
|
+
|
3
|
+
module MrMurano
|
4
|
+
# …/serviceconfig
|
5
|
+
class ServiceConfig < SolutionBase
|
6
|
+
def initialize
|
7
|
+
super
|
8
|
+
@uriparts << 'serviceconfig'
|
9
|
+
end
|
10
|
+
|
11
|
+
def list
|
12
|
+
get()['items']
|
13
|
+
end
|
14
|
+
def fetch(id)
|
15
|
+
get('/' + id.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def assignTriggers(products)
|
20
|
+
scr = list().select{|i| i['service'] == 'device' or i[:service] == 'device'}.first
|
21
|
+
scid = scr['id'] or scr[:id]
|
22
|
+
|
23
|
+
details = Hash.transform_keys_to_symbols(fetch(scid))
|
24
|
+
products = [products] unless products.kind_of? Array
|
25
|
+
details[:triggers] = {:pid=>products}
|
26
|
+
|
27
|
+
put('/'+scid, details)
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def showTriggers
|
32
|
+
scr = list().select{|i| i['service'] == 'device' or i[:service] == 'device'}.first
|
33
|
+
scid = scr['id'] or scr[:id]
|
34
|
+
|
35
|
+
details = Hash.transform_keys_to_symbols(fetch(scid))
|
36
|
+
|
37
|
+
return [] if details[:triggers].nil?
|
38
|
+
details[:triggers][:pid]
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
command :assign do |c|
|
45
|
+
c.syntax = 'mr assign [product]'
|
46
|
+
c.description = 'Assign a product to a eventhandler'
|
47
|
+
|
48
|
+
c.option '--list', %{List assigned products}
|
49
|
+
c.option '--idonly', 'Only return the ids'
|
50
|
+
|
51
|
+
c.action do |args, options|
|
52
|
+
sol = MrMurano::ServiceConfig.new
|
53
|
+
|
54
|
+
if options.list then
|
55
|
+
trigs = sol.showTriggers()
|
56
|
+
if options.idonly then
|
57
|
+
say trigs.join(' ')
|
58
|
+
else
|
59
|
+
acc = MrMurano::Account.new
|
60
|
+
products = acc.products.map{|p| Hash.transform_keys_to_symbols(p)}
|
61
|
+
products.select!{|p| trigs.include? p[:pid] }
|
62
|
+
busy = products.map{|r| [r[:label], r[:type], r[:pid], r[:modelId]]}
|
63
|
+
table = Terminal::Table.new :rows => busy, :headings => ['Label', 'Type', 'PID', 'ModelID']
|
64
|
+
say table
|
65
|
+
end
|
66
|
+
|
67
|
+
else
|
68
|
+
prname = args.shift
|
69
|
+
if prname.nil? then
|
70
|
+
prid = $cfg['product.id']
|
71
|
+
else
|
72
|
+
acc = MrMurano::Account.new
|
73
|
+
products = acc.products.map{|p| Hash.transform_keys_to_symbols(p)}
|
74
|
+
products.select!{|p| p[:label] == prname or p[:pid] == prname }
|
75
|
+
prid = products.map{|p| p[:pid]}
|
76
|
+
end
|
77
|
+
raise "No product ID!" if prid.nil?
|
78
|
+
say "Assigning #{prid} to solution" if $cfg['tool.verbose']
|
79
|
+
sol.assignTriggers(prid) unless $cfg['tool.dry']
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# vim: set ai et sw=2 ts=2 :
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module MrMurano
|
7
|
+
##
|
8
|
+
# Things that servers do that is common.
|
9
|
+
class ServiceBase < SolutionBase
|
10
|
+
# not quite sure why this is needed, but…
|
11
|
+
def mkalias(name)
|
12
|
+
case name
|
13
|
+
when String
|
14
|
+
"/#{$cfg['solution.id']}_#{name}"
|
15
|
+
when Hash
|
16
|
+
if name.has_key? :name then
|
17
|
+
"/#{$cfg['solution.id']}_#{name[:name]}"
|
18
|
+
elsif name.has_key? :service and name.has_key? :event then
|
19
|
+
"/#{$cfg['solution.id']}_#{name[:service]}_#{name[:event]}"
|
20
|
+
else
|
21
|
+
raise "unknown keys. #{name}"
|
22
|
+
end
|
23
|
+
else
|
24
|
+
raise "unknown type. #{name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def list
|
29
|
+
ret = get()
|
30
|
+
ret['items']
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch(name)
|
34
|
+
ret = get('/'+name)
|
35
|
+
if block_given? then
|
36
|
+
yield ret['script']
|
37
|
+
else
|
38
|
+
ret['script']
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# ??? remove
|
43
|
+
def remove(name)
|
44
|
+
delete('/'+name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def upload(local, remote)
|
48
|
+
local = Pathname.new(local) unless local.kind_of? Pathname
|
49
|
+
raise "no file" unless local.exist?
|
50
|
+
|
51
|
+
# we assume these are small enough to slurp.
|
52
|
+
script = local.read
|
53
|
+
|
54
|
+
pst = remote.merge ({
|
55
|
+
:solution_id => $cfg['solution.id'],
|
56
|
+
:script => script
|
57
|
+
})
|
58
|
+
|
59
|
+
# try put, if 404, then post.
|
60
|
+
put(mkalias(remote), pst) do |request, http|
|
61
|
+
response = http.request(request)
|
62
|
+
case response
|
63
|
+
when Net::HTTPSuccess
|
64
|
+
#return JSON.parse(response.body)
|
65
|
+
when Net::HTTPNotFound
|
66
|
+
verbose "Doesn't exist, creating"
|
67
|
+
post('/', pst)
|
68
|
+
else
|
69
|
+
say_error "got #{response} from #{request} #{request.uri.to_s}"
|
70
|
+
say_error ":: #{response.body}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def docmp(itemA, itemB)
|
76
|
+
if itemA[:updated_at].nil? and itemA[:local_path] then
|
77
|
+
itemA[:updated_at] = itemA[:local_path].mtime.getutc
|
78
|
+
elsif itemA[:updated_at].kind_of? String then
|
79
|
+
itemA[:updated_at] = DateTime.parse(itemA[:updated_at]).to_time.getutc
|
80
|
+
end
|
81
|
+
if itemB[:updated_at].nil? and itemB[:local_path] then
|
82
|
+
itemB[:updated_at] = itemB[:local_path].mtime.getutc
|
83
|
+
elsif itemB[:updated_at].kind_of? String then
|
84
|
+
itemB[:updated_at] = DateTime.parse(itemB[:updated_at]).to_time.getutc
|
85
|
+
end
|
86
|
+
return itemA[:updated_at] != itemB[:updated_at]
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# …/library
|
92
|
+
class Library < ServiceBase
|
93
|
+
def initialize
|
94
|
+
super
|
95
|
+
@uriparts << 'library'
|
96
|
+
@itemkey = :alias
|
97
|
+
end
|
98
|
+
|
99
|
+
def tolocalname(item, key)
|
100
|
+
name = item[:name]
|
101
|
+
"#{name}.lua"
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def toremotename(from, path)
|
106
|
+
name = path.basename.to_s.sub(/\..*/, '')
|
107
|
+
{:name => name}
|
108
|
+
end
|
109
|
+
|
110
|
+
def synckey(item)
|
111
|
+
item[:name]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# …/eventhandler
|
116
|
+
class EventHandler < ServiceBase
|
117
|
+
def initialize
|
118
|
+
super
|
119
|
+
@uriparts << 'eventhandler'
|
120
|
+
@itemkey = :alias
|
121
|
+
end
|
122
|
+
|
123
|
+
def list
|
124
|
+
ret = get()
|
125
|
+
skiplist = ($cfg['eventhandler.skiplist'] or '').split
|
126
|
+
ret['items'].reject{|i| i.has_key?('service') and skiplist.include? i['service'] }
|
127
|
+
end
|
128
|
+
|
129
|
+
def fetch(name)
|
130
|
+
ret = get('/'+name)
|
131
|
+
aheader = (ret['script'].lines.first or "").chomp
|
132
|
+
dheader = "--#EVENT #{ret['service']} #{ret['event']}"
|
133
|
+
if block_given? then
|
134
|
+
yield dheader + "\n" if aheader != dheader
|
135
|
+
yield ret['script']
|
136
|
+
else
|
137
|
+
res = ''
|
138
|
+
res << dheader + "\n" if aheader != dheader
|
139
|
+
res << ret['script']
|
140
|
+
res
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def tolocalname(item, key)
|
145
|
+
"#{item[:name]}.lua"
|
146
|
+
end
|
147
|
+
|
148
|
+
def toremotename(from, path)
|
149
|
+
path = Pathname.new(path) unless path.kind_of? Pathname
|
150
|
+
aheader = path.readlines().first
|
151
|
+
md = /--#EVENT (\S+) (\S+)/.match(aheader)
|
152
|
+
raise "Not an Event handler: #{path.to_s}" if md.nil?
|
153
|
+
{:service=>md[1], :event=>md[2]}
|
154
|
+
end
|
155
|
+
|
156
|
+
def synckey(item)
|
157
|
+
"#{item[:service]}_#{item[:event]}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# How do we enable product.id to flow into the eventhandler?
|
162
|
+
end
|
163
|
+
# vim: set ai et sw=2 ts=2 :
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
module MrMurano
|
8
|
+
##
|
9
|
+
# User Management common things
|
10
|
+
class UserBase < SolutionBase
|
11
|
+
def list()
|
12
|
+
get()
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch(id)
|
16
|
+
get('/' + id.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove(id)
|
20
|
+
delete('/' + id.to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
def upload(local, remote)
|
24
|
+
# Roles cannot be modified, so must delete and post.
|
25
|
+
delete('/' + remote[@itemkey]) do |request, http|
|
26
|
+
response = http.request(request)
|
27
|
+
case response
|
28
|
+
when Net::HTTPSuccess
|
29
|
+
when Net::HTTPNotFound
|
30
|
+
else
|
31
|
+
say_error "got #{response} from #{request} #{request.uri.to_s}"
|
32
|
+
say_error ":: #{response.body}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
post('/', remote)
|
36
|
+
end
|
37
|
+
|
38
|
+
def download(local, item)
|
39
|
+
# needs to append/merge with file
|
40
|
+
# for now, we'll read, modify, write
|
41
|
+
here = []
|
42
|
+
if local.exist? then
|
43
|
+
local.open('rb') {|io| here = YAML.load(io)}
|
44
|
+
here = [] if here == false
|
45
|
+
end
|
46
|
+
here.delete_if do |i|
|
47
|
+
Hash.transform_keys_to_symbols(i)[@itemkey] == item[@itemkey]
|
48
|
+
end
|
49
|
+
here << item
|
50
|
+
local.open('wb') do |io|
|
51
|
+
io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def removelocal(dest, item)
|
56
|
+
# needs to append/merge with file
|
57
|
+
# for now, we'll read, modify, write
|
58
|
+
here = []
|
59
|
+
if local.exist? then
|
60
|
+
local.open('rb') {|io| here = YAML.load(io)}
|
61
|
+
here = [] if here == false
|
62
|
+
end
|
63
|
+
key = @itemkey.to_sym
|
64
|
+
here.delete_if do |it|
|
65
|
+
Hash.transform_keys_to_symbols(it)[key] == item[key]
|
66
|
+
end
|
67
|
+
local.open('wb') do|io|
|
68
|
+
io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def tolocalpath(into, item)
|
73
|
+
into
|
74
|
+
end
|
75
|
+
|
76
|
+
def locallist(from)
|
77
|
+
from = Pathname.new(from) unless from.kind_of? Pathname
|
78
|
+
if not from.exist? then
|
79
|
+
say_warning "Skipping missing #{from.to_s}"
|
80
|
+
return []
|
81
|
+
end
|
82
|
+
unless from.file? then
|
83
|
+
say_warning "Cannot read from #{from.to_s}"
|
84
|
+
return []
|
85
|
+
end
|
86
|
+
key = @itemkey.to_sym
|
87
|
+
|
88
|
+
here = []
|
89
|
+
from.open {|io| here = YAML.load(io) }
|
90
|
+
here = [] if here == false
|
91
|
+
|
92
|
+
here
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# …/role
|
97
|
+
class Role < UserBase
|
98
|
+
def initialize
|
99
|
+
super
|
100
|
+
@uriparts << 'role'
|
101
|
+
@itemkey = :role_id
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# …/user
|
106
|
+
class User < UserBase
|
107
|
+
def initialize
|
108
|
+
super
|
109
|
+
@uriparts << 'user'
|
110
|
+
end
|
111
|
+
|
112
|
+
def upload(local, remote)
|
113
|
+
# TODO figure out APIs for updating users.
|
114
|
+
say_warning "Updating Users isn't working currently."
|
115
|
+
# post does work if the :password field is set.
|
116
|
+
end
|
117
|
+
|
118
|
+
def synckey(item)
|
119
|
+
item[:email]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
# vim: set ai et sw=2 ts=2 :
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module MrMurano
|
7
|
+
class SolutionBase
|
8
|
+
# This might also be a valid ProductBase.
|
9
|
+
def initialize
|
10
|
+
@token = Account.new.token
|
11
|
+
@sid = $cfg['solution.id']
|
12
|
+
raise "No solution!" if @sid.nil?
|
13
|
+
@uriparts = [:solution, @sid]
|
14
|
+
@itemkey = :id
|
15
|
+
end
|
16
|
+
|
17
|
+
def verbose(msg)
|
18
|
+
if $cfg['tool.verbose'] then
|
19
|
+
say msg
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def endPoint(path='')
|
24
|
+
parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
|
25
|
+
s = parts.map{|v| v.to_s}.join('/')
|
26
|
+
URI(s + path.to_s)
|
27
|
+
end
|
28
|
+
def http
|
29
|
+
uri = URI('https://' + $cfg['net.host'])
|
30
|
+
if @http.nil? then
|
31
|
+
@http = Net::HTTP.new(uri.host, uri.port)
|
32
|
+
@http.use_ssl = true
|
33
|
+
@http.start
|
34
|
+
end
|
35
|
+
@http
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_req_defaults(request)
|
39
|
+
request.content_type = 'application/json'
|
40
|
+
request['authorization'] = 'token ' + @token
|
41
|
+
request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
|
42
|
+
request
|
43
|
+
end
|
44
|
+
|
45
|
+
def workit(request, &block)
|
46
|
+
set_req_defaults(request)
|
47
|
+
if block_given? then
|
48
|
+
yield request, http()
|
49
|
+
else
|
50
|
+
response = http().request(request)
|
51
|
+
case response
|
52
|
+
when Net::HTTPSuccess
|
53
|
+
return {} if response.body.nil?
|
54
|
+
begin
|
55
|
+
return JSON.parse(response.body)
|
56
|
+
rescue
|
57
|
+
return response.body
|
58
|
+
end
|
59
|
+
else
|
60
|
+
say_error "got #{response} from #{request} #{request.uri.to_s}"
|
61
|
+
say_error ":: #{response.body}"
|
62
|
+
raise response
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(path='', &block)
|
68
|
+
uri = endPoint(path)
|
69
|
+
workit(Net::HTTP::Get.new(uri), &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def post(path='', body={}, &block)
|
73
|
+
uri = endPoint(path)
|
74
|
+
req = Net::HTTP::Post.new(uri)
|
75
|
+
req.body = JSON.generate(body)
|
76
|
+
workit(req, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
def put(path='', body={}, &block)
|
80
|
+
uri = endPoint(path)
|
81
|
+
req = Net::HTTP::Put.new(uri)
|
82
|
+
req.body = JSON.generate(body)
|
83
|
+
workit(req, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete(path='', &block)
|
87
|
+
uri = endPoint(path)
|
88
|
+
workit(Net::HTTP::Delete.new(uri), &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
# …
|
92
|
+
|
93
|
+
def toremotename(root, path)
|
94
|
+
path = Pathname.new(path) unless path.kind_of? Pathname
|
95
|
+
root = Pathname.new(root) unless root.kind_of? Pathname
|
96
|
+
path.relative_path_from(root).to_s
|
97
|
+
end
|
98
|
+
def tolocalpath(into, item)
|
99
|
+
into.mkpath unless $cfg['tool.dry']
|
100
|
+
return item[:local_path] if item.has_key? :local_path
|
101
|
+
itemkey = @itemkey.to_sym
|
102
|
+
name = tolocalname(item, itemkey)
|
103
|
+
raise "Bad key(#{itemkey}) for #{item}" if name.nil?
|
104
|
+
dest = into + name
|
105
|
+
end
|
106
|
+
|
107
|
+
def locallist(from)
|
108
|
+
from = Pathname.new(from) unless from.kind_of? Pathname
|
109
|
+
unless from.exist? then
|
110
|
+
return []
|
111
|
+
end
|
112
|
+
raise "Not a directory: #{from.to_s}" unless from.directory?
|
113
|
+
|
114
|
+
Pathname.glob(from.to_s + '/**/*').map do |path|
|
115
|
+
name = toremotename(from, path)
|
116
|
+
case name
|
117
|
+
when Hash
|
118
|
+
name[:local_path] = path
|
119
|
+
name
|
120
|
+
else
|
121
|
+
{:local_path => path, :name => name}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def synckey(item)
|
127
|
+
key = @itemkey.to_sym
|
128
|
+
item[key]
|
129
|
+
end
|
130
|
+
|
131
|
+
def syncup(from, options={})
|
132
|
+
there = list()
|
133
|
+
here = locallist(from)
|
134
|
+
itemkey = @itemkey.to_sym
|
135
|
+
|
136
|
+
# split into three lists.
|
137
|
+
# - Items here and not there. (toadd)
|
138
|
+
# - Items there and not here. (todel)
|
139
|
+
# - Items here and there. (tomod)
|
140
|
+
therebox = {}
|
141
|
+
there.each do |item|
|
142
|
+
item = Hash.transform_keys_to_symbols(item)
|
143
|
+
therebox[ synckey(item) ] = item
|
144
|
+
end
|
145
|
+
herebox = {}
|
146
|
+
here.each do |item|
|
147
|
+
item = Hash.transform_keys_to_symbols(item)
|
148
|
+
herebox[ synckey(item) ] = item
|
149
|
+
end
|
150
|
+
toadd = herebox.keys - therebox.keys
|
151
|
+
todel = therebox.keys - herebox.keys
|
152
|
+
tomod = herebox.keys & therebox.keys
|
153
|
+
|
154
|
+
if options.delete then
|
155
|
+
todel.each do |key|
|
156
|
+
verbose "Removing item #{key}"
|
157
|
+
unless $cfg['tool.dry'] then
|
158
|
+
item = therebox[key]
|
159
|
+
remove(item[itemkey])
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
if options.create then
|
164
|
+
toadd.each do |key|
|
165
|
+
verbose "Adding item #{key}"
|
166
|
+
unless $cfg['tool.dry'] then
|
167
|
+
item = herebox[key]
|
168
|
+
upload(item[:local_path], item.reject{|k,v| k==:local_path})
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
if options.update then
|
173
|
+
tomod.each do |key|
|
174
|
+
verbose "Updating item #{key}"
|
175
|
+
unless $cfg['tool.dry'] then
|
176
|
+
#item = therebox[key].merge herebox[key] # need to be consistent with key types for this to work
|
177
|
+
id = therebox[key][itemkey]
|
178
|
+
item = herebox[key].dup
|
179
|
+
item[itemkey] = id
|
180
|
+
upload(item[:local_path], item.reject{|k,v| k==:local_path})
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def syncdown(into, options={})
|
187
|
+
there = list()
|
188
|
+
into = Pathname.new(into) unless into.kind_of? Pathname
|
189
|
+
here = locallist(into)
|
190
|
+
itemkey = @itemkey.to_sym
|
191
|
+
|
192
|
+
# split into three lists.
|
193
|
+
# - Items here and not there. (todel)
|
194
|
+
# - Items there and not here. (toadd)
|
195
|
+
# - Items here and there. (tomod)
|
196
|
+
therebox = {}
|
197
|
+
there.each do |item|
|
198
|
+
item = Hash.transform_keys_to_symbols(item)
|
199
|
+
therebox[ synckey(item) ] = item
|
200
|
+
end
|
201
|
+
herebox = {}
|
202
|
+
here.each do |item|
|
203
|
+
item = Hash.transform_keys_to_symbols(item)
|
204
|
+
herebox[ synckey(item) ] = item
|
205
|
+
end
|
206
|
+
todel = herebox.keys - therebox.keys
|
207
|
+
toadd = therebox.keys - herebox.keys
|
208
|
+
tomod = herebox.keys & therebox.keys
|
209
|
+
|
210
|
+
if options.delete then
|
211
|
+
todel.each do |key|
|
212
|
+
verbose "Removing item #{key}"
|
213
|
+
unless $cfg['tool.dry'] then
|
214
|
+
item = herebox[key]
|
215
|
+
dest = tolocalpath(into, item)
|
216
|
+
removelocal(dest, item)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
if options.create then
|
221
|
+
toadd.each do |key|
|
222
|
+
verbose "Adding item #{key}"
|
223
|
+
unless $cfg['tool.dry'] then
|
224
|
+
item = therebox[key]
|
225
|
+
dest = tolocalpath(into, item)
|
226
|
+
|
227
|
+
download(dest, item)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
if options.update then
|
232
|
+
tomod.each do |key|
|
233
|
+
verbose "Updating item #{key}"
|
234
|
+
unless $cfg['tool.dry'] then
|
235
|
+
item = therebox[key]
|
236
|
+
dest = tolocalpath(into, herebox[key].merge(item) )
|
237
|
+
|
238
|
+
download(dest, item)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def download(local, item)
|
245
|
+
id = item[@itemkey.to_sym]
|
246
|
+
local.open('wb') do |io|
|
247
|
+
fetch(id) do |chunk|
|
248
|
+
io.write chunk
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def removelocal(dest, item)
|
254
|
+
dest.unlink
|
255
|
+
end
|
256
|
+
|
257
|
+
def status(from, options={})
|
258
|
+
there = list()
|
259
|
+
here = locallist(from)
|
260
|
+
itemkey = @itemkey.to_sym
|
261
|
+
|
262
|
+
therebox = {}
|
263
|
+
there.each do |item|
|
264
|
+
item = Hash.transform_keys_to_symbols(item)
|
265
|
+
item[:synckey] = synckey(item)
|
266
|
+
therebox[ item[:synckey] ] = item
|
267
|
+
end
|
268
|
+
herebox = {}
|
269
|
+
here.each do |item|
|
270
|
+
item = Hash.transform_keys_to_symbols(item)
|
271
|
+
item[:synckey] = synckey(item)
|
272
|
+
herebox[ item[:synckey] ] = item
|
273
|
+
end
|
274
|
+
if options.asdown then
|
275
|
+
todel = herebox.keys - therebox.keys
|
276
|
+
toadd = therebox.keys - herebox.keys
|
277
|
+
tomod = herebox.keys & therebox.keys
|
278
|
+
{
|
279
|
+
:toadd=> toadd.map{|key| therebox[key] },
|
280
|
+
:todel=> todel.map{|key| herebox[key] },
|
281
|
+
# FIXME what if therebox[key] is nil?
|
282
|
+
:tomod=> tomod.map{|key| therebox[key].merge(herebox[key]) }
|
283
|
+
}
|
284
|
+
else
|
285
|
+
toadd = herebox.keys - therebox.keys
|
286
|
+
todel = therebox.keys - herebox.keys
|
287
|
+
tomod = herebox.keys & therebox.keys
|
288
|
+
{
|
289
|
+
:toadd=> toadd.map{|key| herebox[key] },
|
290
|
+
:todel=> todel.map{|key| therebox[key] },
|
291
|
+
:tomod=> tomod.map{|key| therebox[key].merge(herebox[key]) }
|
292
|
+
}
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class Solution < SolutionBase
|
298
|
+
def version
|
299
|
+
get('/version')
|
300
|
+
end
|
301
|
+
|
302
|
+
def info
|
303
|
+
get()
|
304
|
+
end
|
305
|
+
|
306
|
+
def list
|
307
|
+
get('/')
|
308
|
+
end
|
309
|
+
|
310
|
+
def log
|
311
|
+
get('/logs')
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
# vim: set ai et sw=2 ts=2 :
|