MuranoCLI 2.0.0
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 +7 -0
- data/.gitignore +28 -0
- data/.rspec +2 -0
- data/.travis.yml +21 -0
- data/Gemfile +27 -0
- data/LICENSE.txt +19 -0
- data/MuranoCLI.gemspec +50 -0
- data/MuranoCLI.iss +50 -0
- data/README.markdown +208 -0
- data/Rakefile +188 -0
- data/TODO.taskpaper +122 -0
- data/bin/mr +8 -0
- data/bin/murano +84 -0
- data/docs/demo.md +109 -0
- data/lib/MrMurano/Account.rb +211 -0
- data/lib/MrMurano/Config-Migrate.rb +47 -0
- data/lib/MrMurano/Config.rb +286 -0
- data/lib/MrMurano/Mock.rb +63 -0
- data/lib/MrMurano/Product-1P-Device.rb +145 -0
- data/lib/MrMurano/Product-Resources.rb +195 -0
- data/lib/MrMurano/Product.rb +358 -0
- data/lib/MrMurano/ProjectFile.rb +349 -0
- data/lib/MrMurano/Solution-Cors.rb +46 -0
- data/lib/MrMurano/Solution-Endpoint.rb +177 -0
- data/lib/MrMurano/Solution-File.rb +150 -0
- data/lib/MrMurano/Solution-ServiceConfig.rb +140 -0
- data/lib/MrMurano/Solution-Services.rb +326 -0
- data/lib/MrMurano/Solution-Users.rb +129 -0
- data/lib/MrMurano/Solution.rb +59 -0
- data/lib/MrMurano/SubCmdGroupContext.rb +49 -0
- data/lib/MrMurano/SyncUpDown.rb +565 -0
- data/lib/MrMurano/commands/assign.rb +57 -0
- data/lib/MrMurano/commands/businessList.rb +45 -0
- data/lib/MrMurano/commands/completion.rb +152 -0
- data/lib/MrMurano/commands/config.rb +67 -0
- data/lib/MrMurano/commands/content.rb +130 -0
- data/lib/MrMurano/commands/cors.rb +30 -0
- data/lib/MrMurano/commands/domain.rb +17 -0
- data/lib/MrMurano/commands/gb.rb +33 -0
- data/lib/MrMurano/commands/init.rb +138 -0
- data/lib/MrMurano/commands/keystore.rb +157 -0
- data/lib/MrMurano/commands/logs.rb +78 -0
- data/lib/MrMurano/commands/mock.rb +63 -0
- data/lib/MrMurano/commands/password.rb +88 -0
- data/lib/MrMurano/commands/postgresql.rb +41 -0
- data/lib/MrMurano/commands/product.rb +14 -0
- data/lib/MrMurano/commands/productCreate.rb +39 -0
- data/lib/MrMurano/commands/productDelete.rb +33 -0
- data/lib/MrMurano/commands/productDevice.rb +84 -0
- data/lib/MrMurano/commands/productDeviceIdCmds.rb +86 -0
- data/lib/MrMurano/commands/productList.rb +45 -0
- data/lib/MrMurano/commands/productWrite.rb +27 -0
- data/lib/MrMurano/commands/show.rb +80 -0
- data/lib/MrMurano/commands/solution.rb +14 -0
- data/lib/MrMurano/commands/solutionCreate.rb +39 -0
- data/lib/MrMurano/commands/solutionDelete.rb +34 -0
- data/lib/MrMurano/commands/solutionList.rb +45 -0
- data/lib/MrMurano/commands/status.rb +92 -0
- data/lib/MrMurano/commands/sync.rb +60 -0
- data/lib/MrMurano/commands/timeseries.rb +115 -0
- data/lib/MrMurano/commands/tsdb.rb +271 -0
- data/lib/MrMurano/commands/usage.rb +23 -0
- data/lib/MrMurano/commands/zshcomplete.erb +112 -0
- data/lib/MrMurano/commands.rb +32 -0
- data/lib/MrMurano/hash.rb +20 -0
- data/lib/MrMurano/http.rb +153 -0
- data/lib/MrMurano/makePretty.rb +75 -0
- data/lib/MrMurano/schema/pf-v1.0.0.yaml +114 -0
- data/lib/MrMurano/schema/sf-v0.2.0.yaml +77 -0
- data/lib/MrMurano/schema/sf-v0.3.0.yaml +78 -0
- data/lib/MrMurano/template/mock.erb +9 -0
- data/lib/MrMurano/template/projectFile.murano.erb +81 -0
- data/lib/MrMurano/verbosing.rb +99 -0
- data/lib/MrMurano/version.rb +4 -0
- data/lib/MrMurano.rb +20 -0
- data/spec/Account-Passwords_spec.rb +242 -0
- data/spec/Account_spec.rb +272 -0
- data/spec/ConfigFile_spec.rb +50 -0
- data/spec/ConfigMigrate_spec.rb +89 -0
- data/spec/Config_spec.rb +409 -0
- data/spec/Http_spec.rb +204 -0
- data/spec/MakePretties_spec.rb +118 -0
- data/spec/Mock_spec.rb +53 -0
- data/spec/ProductBase_spec.rb +113 -0
- data/spec/ProductContent_spec.rb +162 -0
- data/spec/ProductResources_spec.rb +329 -0
- data/spec/Product_1P_Device_spec.rb +202 -0
- data/spec/Product_1P_RPC_spec.rb +175 -0
- data/spec/Product_spec.rb +153 -0
- data/spec/ProjectFile_spec.rb +324 -0
- data/spec/Solution-Cors_spec.rb +164 -0
- data/spec/Solution-Endpoint_spec.rb +581 -0
- data/spec/Solution-File_spec.rb +212 -0
- data/spec/Solution-ServiceConfig_spec.rb +202 -0
- data/spec/Solution-ServiceDevice_spec.rb +176 -0
- data/spec/Solution-ServiceEventHandler_spec.rb +385 -0
- data/spec/Solution-ServiceModules_spec.rb +465 -0
- data/spec/Solution-UsersRoles_spec.rb +207 -0
- data/spec/Solution_spec.rb +92 -0
- data/spec/SyncRoot_spec.rb +83 -0
- data/spec/SyncUpDown_spec.rb +495 -0
- data/spec/Verbosing_spec.rb +279 -0
- data/spec/_workspace.rb +27 -0
- data/spec/cmd_assign_spec.rb +51 -0
- data/spec/cmd_business_spec.rb +59 -0
- data/spec/cmd_common.rb +72 -0
- data/spec/cmd_config_spec.rb +68 -0
- data/spec/cmd_content_spec.rb +71 -0
- data/spec/cmd_cors_spec.rb +50 -0
- data/spec/cmd_device_spec.rb +96 -0
- data/spec/cmd_domain_spec.rb +32 -0
- data/spec/cmd_init_spec.rb +30 -0
- data/spec/cmd_keystore_spec.rb +97 -0
- data/spec/cmd_password_spec.rb +62 -0
- data/spec/cmd_status_spec.rb +239 -0
- data/spec/cmd_syncdown_spec.rb +86 -0
- data/spec/cmd_syncup_spec.rb +62 -0
- data/spec/cmd_usage_spec.rb +36 -0
- data/spec/fixtures/.mrmuranorc +9 -0
- data/spec/fixtures/ProjectFiles/invalid.yaml +9 -0
- data/spec/fixtures/ProjectFiles/only_meta.yaml +24 -0
- data/spec/fixtures/ProjectFiles/with_routes.yaml +27 -0
- data/spec/fixtures/SolutionFiles/0.2.0.json +20 -0
- data/spec/fixtures/SolutionFiles/0.2.0_invalid.json +18 -0
- data/spec/fixtures/SolutionFiles/0.2.json +21 -0
- data/spec/fixtures/SolutionFiles/0.3.0.json +20 -0
- data/spec/fixtures/SolutionFiles/0.3.0_invalid.json +19 -0
- data/spec/fixtures/SolutionFiles/0.3.json +20 -0
- data/spec/fixtures/SolutionFiles/basic.json +20 -0
- data/spec/fixtures/SolutionFiles/secret.json +6 -0
- data/spec/fixtures/configfile +9 -0
- data/spec/fixtures/dumped_config +42 -0
- data/spec/fixtures/mrmuranorc_deleted_bob +8 -0
- data/spec/fixtures/mrmuranorc_tool_bob +3 -0
- data/spec/fixtures/product_spec_files/example.exoline.spec.yaml +116 -0
- data/spec/fixtures/product_spec_files/example.murano.spec.yaml +14 -0
- data/spec/fixtures/product_spec_files/gwe.exoline.spec.yaml +21 -0
- data/spec/fixtures/product_spec_files/gwe.murano.spec.yaml +16 -0
- data/spec/fixtures/product_spec_files/lightbulb-no-state.yaml +11 -0
- data/spec/fixtures/product_spec_files/lightbulb.yaml +14 -0
- data/spec/fixtures/roles-three.yaml +11 -0
- data/spec/fixtures/syncable_content/assets/icon.png +0 -0
- data/spec/fixtures/syncable_content/assets/index.html +0 -0
- data/spec/fixtures/syncable_content/assets/js/script.js +0 -0
- data/spec/fixtures/syncable_content/modules/table_util.lua +58 -0
- data/spec/fixtures/syncable_content/routes/manyRoutes.lua +11 -0
- data/spec/fixtures/syncable_content/routes/singleRoute.lua +5 -0
- data/spec/fixtures/syncable_content/services/devdata.lua +18 -0
- data/spec/fixtures/syncable_content/services/timers.lua +4 -0
- data/spec/spec_helper.rb +119 -0
- metadata +498 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'pp'
|
|
6
|
+
require 'MrMurano/Solution'
|
|
7
|
+
|
|
8
|
+
module MrMurano
|
|
9
|
+
##
|
|
10
|
+
# User Management common things
|
|
11
|
+
class UserBase < SolutionBase
|
|
12
|
+
def list()
|
|
13
|
+
get()
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch(id)
|
|
17
|
+
get('/' + id.to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove(id)
|
|
21
|
+
delete('/' + id.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param modify Bool: True if item exists already and this is changing it
|
|
25
|
+
def upload(local, remote, modify)
|
|
26
|
+
# Roles cannot be modified, so must delete and post.
|
|
27
|
+
delete('/' + remote[@itemkey]) do |request, http|
|
|
28
|
+
response = http.request(request)
|
|
29
|
+
case response
|
|
30
|
+
when Net::HTTPSuccess
|
|
31
|
+
when Net::HTTPNotFound
|
|
32
|
+
else
|
|
33
|
+
showHttpError(request, response)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
remote.reject!{|k,v| k==:synckey or k==:bundled}
|
|
37
|
+
post('/', remote)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def download(local, item)
|
|
41
|
+
# needs to append/merge with file
|
|
42
|
+
# for now, we'll read, modify, write
|
|
43
|
+
here = []
|
|
44
|
+
if local.exist? then
|
|
45
|
+
local.open('rb') {|io| here = YAML.load(io)}
|
|
46
|
+
here = [] if here == false
|
|
47
|
+
end
|
|
48
|
+
here.delete_if do |i|
|
|
49
|
+
Hash.transform_keys_to_symbols(i)[@itemkey] == item[@itemkey]
|
|
50
|
+
end
|
|
51
|
+
here << item.reject{|k,v| k==:synckey}
|
|
52
|
+
local.open('wb') do |io|
|
|
53
|
+
io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def removelocal(local, item)
|
|
58
|
+
# needs to append/merge with file
|
|
59
|
+
# for now, we'll read, modify, write
|
|
60
|
+
here = []
|
|
61
|
+
if local.exist? then
|
|
62
|
+
local.open('rb') {|io| here = YAML.load(io)}
|
|
63
|
+
here = [] if here == false
|
|
64
|
+
end
|
|
65
|
+
key = @itemkey.to_sym
|
|
66
|
+
here.delete_if do |it|
|
|
67
|
+
Hash.transform_keys_to_symbols(it)[key] == item[key]
|
|
68
|
+
end
|
|
69
|
+
local.open('wb') do|io|
|
|
70
|
+
io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tolocalpath(into, item)
|
|
75
|
+
into
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def localitems(from)
|
|
79
|
+
from = Pathname.new(from) unless from.kind_of? Pathname
|
|
80
|
+
if not from.exist? then
|
|
81
|
+
warning "Skipping missing #{from.to_s}"
|
|
82
|
+
return []
|
|
83
|
+
end
|
|
84
|
+
unless from.file? then
|
|
85
|
+
warning "Cannot read from #{from.to_s}"
|
|
86
|
+
return []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
here = []
|
|
90
|
+
from.open {|io| here = YAML.load(io) }
|
|
91
|
+
here = [] if here == false
|
|
92
|
+
|
|
93
|
+
here.map{|i| Hash.transform_keys_to_symbols(i)}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# …/role
|
|
98
|
+
class Role < UserBase
|
|
99
|
+
def initialize
|
|
100
|
+
super
|
|
101
|
+
@uriparts << 'role'
|
|
102
|
+
@itemkey = :role_id
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
#SyncRoot.add('roles', Role, 'R', %{Roles})
|
|
106
|
+
|
|
107
|
+
# …/user
|
|
108
|
+
# :nocov:
|
|
109
|
+
class User < UserBase
|
|
110
|
+
def initialize
|
|
111
|
+
super
|
|
112
|
+
@uriparts << 'user'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param modify Bool: True if item exists already and this is changing it
|
|
116
|
+
def upload(local, remote, modify)
|
|
117
|
+
# TODO figure out APIs for updating users.
|
|
118
|
+
warning "Updating Users isn't working currently."
|
|
119
|
+
# post does work if the :password field is set.
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def synckey(item)
|
|
123
|
+
item[:email]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
# :nocov:
|
|
127
|
+
#SyncRoot.add('users', User, 'U', %{Users})
|
|
128
|
+
end
|
|
129
|
+
# vim: set ai et sw=2 ts=2 :
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'MrMurano/Config'
|
|
3
|
+
require 'MrMurano/http'
|
|
4
|
+
require 'MrMurano/verbosing'
|
|
5
|
+
require 'MrMurano/SyncUpDown'
|
|
6
|
+
|
|
7
|
+
module MrMurano
|
|
8
|
+
class SolutionBase
|
|
9
|
+
def initialize
|
|
10
|
+
@sid = $cfg['solution.id']
|
|
11
|
+
raise "No solution!" if @sid.nil?
|
|
12
|
+
@uriparts = [:solution, @sid]
|
|
13
|
+
@itemkey = :id
|
|
14
|
+
@project_section = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
include Http
|
|
18
|
+
include Verbose
|
|
19
|
+
|
|
20
|
+
## Generate an endpoint in Murano
|
|
21
|
+
# Uses the uriparts and path
|
|
22
|
+
# @param path String: any additional parts for the URI
|
|
23
|
+
# @return URI: The full URI for this enpoint.
|
|
24
|
+
def endPoint(path='')
|
|
25
|
+
parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
|
|
26
|
+
s = parts.map{|v| v.to_s}.join('/')
|
|
27
|
+
URI(s + path.to_s)
|
|
28
|
+
end
|
|
29
|
+
# …
|
|
30
|
+
|
|
31
|
+
include SyncUpDown
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Solution < SolutionBase
|
|
35
|
+
def version
|
|
36
|
+
get('/version')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def info
|
|
40
|
+
get()
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def list
|
|
44
|
+
get('/')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def log
|
|
48
|
+
get('/logs')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def usage
|
|
52
|
+
get('/usage')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# vim: set ai et sw=2 ts=2 :
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
module MrMurano
|
|
3
|
+
class SubCmdGroupHelp
|
|
4
|
+
attr :name, :description
|
|
5
|
+
|
|
6
|
+
def initialize(command)
|
|
7
|
+
@name = command.syntax.to_s
|
|
8
|
+
@description = command.description.to_s
|
|
9
|
+
@runner = ::Commander::Runner.instance
|
|
10
|
+
prefix = /^#{command.name.to_s} /
|
|
11
|
+
cmds = @runner.instance_variable_get(:@commands).select{|n,_| n.to_s =~ prefix}
|
|
12
|
+
@commands = cmds
|
|
13
|
+
als = @runner.instance_variable_get(:@aliases).select{|n,_| n.to_s =~ prefix}
|
|
14
|
+
@aliases = als
|
|
15
|
+
|
|
16
|
+
@options = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def program(key)
|
|
20
|
+
case key
|
|
21
|
+
when :name
|
|
22
|
+
@name
|
|
23
|
+
when :description
|
|
24
|
+
@description
|
|
25
|
+
when :help
|
|
26
|
+
nil
|
|
27
|
+
else
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def alias?(name)
|
|
33
|
+
@aliases.include? name.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def command(name)
|
|
37
|
+
@commands[name.to_s]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_help
|
|
41
|
+
hf = @runner.program(:help_formatter).new(self)
|
|
42
|
+
pc = Commander::HelpFormatter::ProgramContext.new(self).get_binding
|
|
43
|
+
hf.template(:help).result(pc)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# vim: set ai et sw=2 ts=2 :
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'MrMurano/Config'
|
|
6
|
+
require 'MrMurano/ProjectFile'
|
|
7
|
+
require 'MrMurano/hash'
|
|
8
|
+
|
|
9
|
+
module MrMurano
|
|
10
|
+
## Track what things are syncable.
|
|
11
|
+
class SyncRoot
|
|
12
|
+
Syncable = Struct.new(:name, :class, :type, :desc, :bydefault) do
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Add a new entry to syncable things
|
|
17
|
+
# +name+:: The name to use for the long option
|
|
18
|
+
# +klass+:: The class to instanciate from
|
|
19
|
+
# +type+:: Single letter for short option and status listing
|
|
20
|
+
# +desc+:: Summary of what this syncs.
|
|
21
|
+
# +bydefault+:: Is this part of the default sync group
|
|
22
|
+
#
|
|
23
|
+
# returns nil
|
|
24
|
+
def self.add(name, klass, type, desc, bydefault=false)
|
|
25
|
+
@@syncset = [] unless defined?(@@syncset)
|
|
26
|
+
@@syncset << Syncable.new(name.to_s, klass, type, desc, bydefault)
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Remove all syncables.
|
|
32
|
+
def self.reset()
|
|
33
|
+
@@syncset = []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Get the list of default syncables.
|
|
38
|
+
# returns array of names
|
|
39
|
+
def self.bydefault
|
|
40
|
+
@@syncset.select{|a| a.bydefault }.map{|a| a.name}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Iterate over all syncables
|
|
45
|
+
# +block+:: code to run on each
|
|
46
|
+
def self.each(&block)
|
|
47
|
+
@@syncset.each{|a| yield a.name, a.type, a.class }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Iterate over all syncables with option arguments.
|
|
52
|
+
# +block+:: code to run on each
|
|
53
|
+
def self.each_option(&block)
|
|
54
|
+
@@syncset.each{|a| yield "-#{a.type.downcase}", "--[no-]#{a.name}", a.desc}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Iterate over just the selected syncables.
|
|
59
|
+
# +opt+:: Options hash of which to select from
|
|
60
|
+
# +block+:: code to run on each
|
|
61
|
+
def self.each_filtered(opt, &block)
|
|
62
|
+
self.checkSAME(opt)
|
|
63
|
+
@@syncset.each do |a|
|
|
64
|
+
if opt[a.name.to_sym] or opt[a.type.to_sym] then
|
|
65
|
+
yield a.name, a.type, a.class
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
## Adjust options based on all or none
|
|
71
|
+
# If none are selected, select the bydefault ones.
|
|
72
|
+
#
|
|
73
|
+
# +opt+:: Options hash of which to select from
|
|
74
|
+
#
|
|
75
|
+
# returns nil
|
|
76
|
+
def self.checkSAME(opt)
|
|
77
|
+
if opt[:all] then
|
|
78
|
+
@@syncset.each {|a| opt[a.name.to_sym] = true }
|
|
79
|
+
else
|
|
80
|
+
any = @@syncset.select {|a| opt[a.name.to_sym] or opt[a.type.to_sym]}
|
|
81
|
+
if any.empty? then
|
|
82
|
+
bydef = $cfg['sync.bydefault'].split
|
|
83
|
+
@@syncset.select{|a| bydef.include? a.name }.each{|a| opt[a.name.to_sym] = true}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
module SyncUpDown
|
|
92
|
+
#######################################################################
|
|
93
|
+
# Methods that must be overridden
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Get a list of remote items.
|
|
97
|
+
#
|
|
98
|
+
# Children objects Must override this
|
|
99
|
+
#
|
|
100
|
+
# @return Array: of Hashes of item details
|
|
101
|
+
def list()
|
|
102
|
+
[]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
## Remove remote item
|
|
106
|
+
#
|
|
107
|
+
# Children objects Must override this
|
|
108
|
+
#
|
|
109
|
+
# @param itemkey String: The identifying key for this item
|
|
110
|
+
def remove(itemkey)
|
|
111
|
+
# :nocov:
|
|
112
|
+
raise "Forgotten implementation"
|
|
113
|
+
# :nocov:
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
## Upload local item to remote
|
|
117
|
+
#
|
|
118
|
+
# Children objects Must override this
|
|
119
|
+
#
|
|
120
|
+
# @param src Pathname: Full path of where to upload from
|
|
121
|
+
# @param item Hash: The item details to upload
|
|
122
|
+
# @param modify Bool: True if item exists already and this is changing it
|
|
123
|
+
def upload(src, item, modify)
|
|
124
|
+
# :nocov:
|
|
125
|
+
raise "Forgotten implementation"
|
|
126
|
+
# :nocov:
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# True if itemA and itemB are different
|
|
131
|
+
#
|
|
132
|
+
# Children objects must override this
|
|
133
|
+
#
|
|
134
|
+
def docmp(itemA, itemB)
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
#######################################################################
|
|
140
|
+
|
|
141
|
+
#######################################################################
|
|
142
|
+
# Methods that could be overriden
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Compute a remote item hash from the local path
|
|
146
|
+
#
|
|
147
|
+
# Children objects should override this.
|
|
148
|
+
#
|
|
149
|
+
# @param root [Pathname,String] Root path for this resource type from config files
|
|
150
|
+
# @param path [Pathname,String] Path to local item
|
|
151
|
+
# @return [Hash] hash of the details for the remote item for this path
|
|
152
|
+
def toRemoteItem(root, path)
|
|
153
|
+
# This mess brought to you by Windows short path names.
|
|
154
|
+
path = Dir.glob(path.to_s).first
|
|
155
|
+
root = Dir.glob(root.to_s).first
|
|
156
|
+
path = Pathname.new(path)
|
|
157
|
+
root = Pathname.new(root)
|
|
158
|
+
{:name => path.realpath.relative_path_from(root.realpath).to_s}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# Compute the local name from remote item details
|
|
163
|
+
#
|
|
164
|
+
# Children objects should override this or #tolocalpath
|
|
165
|
+
#
|
|
166
|
+
# @param item Hash: listing details for the item.
|
|
167
|
+
# @param itemkey Symbol: Key for look up.
|
|
168
|
+
def tolocalname(item, itemkey)
|
|
169
|
+
item[itemkey].to_s
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
##
|
|
173
|
+
# Compute the local path from the listing details
|
|
174
|
+
#
|
|
175
|
+
# If there is already a matching local item, some of its details are also in
|
|
176
|
+
# the item hash.
|
|
177
|
+
#
|
|
178
|
+
# Children objects should override this or #tolocalname
|
|
179
|
+
#
|
|
180
|
+
# @param into Pathname: Root path for this resource type from config files
|
|
181
|
+
# @param item Hash: listing details for the item.
|
|
182
|
+
# @return Pathname: path to save (or merge) remote item into
|
|
183
|
+
def tolocalpath(into, item)
|
|
184
|
+
return item[:local_path] if item.has_key? :local_path
|
|
185
|
+
itemkey = @itemkey.to_sym
|
|
186
|
+
name = tolocalname(item, itemkey)
|
|
187
|
+
raise "Bad key(#{itemkey}) for #{item}" if name.nil?
|
|
188
|
+
name = Pathname.new(name) unless name.kind_of? Pathname
|
|
189
|
+
name = name.relative_path_from(Pathname.new('/')) if name.absolute?
|
|
190
|
+
into + name
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
## Does item match pattern?
|
|
194
|
+
#
|
|
195
|
+
# Children objects should override this if synckey is not @itemkey
|
|
196
|
+
#
|
|
197
|
+
# Check child specific patterns against item
|
|
198
|
+
#
|
|
199
|
+
# @returns true or false
|
|
200
|
+
def match(item, pattern)
|
|
201
|
+
false
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
## Get the key used to quickly compare two items
|
|
205
|
+
#
|
|
206
|
+
# Children objects should override this if synckey is not @itemkey
|
|
207
|
+
#
|
|
208
|
+
# @param item Hash: The item to get a key from
|
|
209
|
+
# @returns Object: The object to use a comparison key
|
|
210
|
+
def synckey(item)
|
|
211
|
+
key = @itemkey.to_sym
|
|
212
|
+
item[key]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
## Download an item into local
|
|
216
|
+
#
|
|
217
|
+
# Children objects should override this or implement #fetch()
|
|
218
|
+
#
|
|
219
|
+
# @param local Pathname: Full path of where to download to
|
|
220
|
+
# @param item Hash: The item to download
|
|
221
|
+
def download(local, item)
|
|
222
|
+
if item[:bundled] then
|
|
223
|
+
warning "Not downloading into bundled item #{synckey(item)}"
|
|
224
|
+
return
|
|
225
|
+
end
|
|
226
|
+
local.dirname.mkpath
|
|
227
|
+
id = item[@itemkey.to_sym]
|
|
228
|
+
if id.nil? then
|
|
229
|
+
debug "!!! Missing '#{@itemkey}', using :id instead!"
|
|
230
|
+
debug ":id => #{item[:id]}"
|
|
231
|
+
id = item[:id]
|
|
232
|
+
raise "Both #{@itemkey} and id in item are nil!" if id.nil?
|
|
233
|
+
end
|
|
234
|
+
local.open('wb') do |io|
|
|
235
|
+
fetch(id) do |chunk|
|
|
236
|
+
io.write chunk
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
## Remove local reference of item
|
|
242
|
+
#
|
|
243
|
+
# Children objects should override this if move than just unlinking the local
|
|
244
|
+
# item.
|
|
245
|
+
#
|
|
246
|
+
# @param dest Pathname: Full path of item to be removed
|
|
247
|
+
# @param item Hash: Full details of item to be removed
|
|
248
|
+
def removelocal(dest, item)
|
|
249
|
+
dest.unlink
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
#
|
|
253
|
+
#######################################################################
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
##
|
|
257
|
+
# So, for bundles this needs to look at all the places and build up the mered
|
|
258
|
+
# stack of local items.
|
|
259
|
+
#
|
|
260
|
+
# Which means it needs the from to be split into the base and the sub so we can
|
|
261
|
+
# inject bundle directories.
|
|
262
|
+
|
|
263
|
+
##
|
|
264
|
+
# Get a list of local items.
|
|
265
|
+
#
|
|
266
|
+
# Children should never need to override this. Instead they should override
|
|
267
|
+
# #localitems
|
|
268
|
+
#
|
|
269
|
+
# This collects items in the project and all bundles.
|
|
270
|
+
# @return Array: of Hashes of items
|
|
271
|
+
def locallist()
|
|
272
|
+
# so. if @locationbase/bundles exists
|
|
273
|
+
# gather and merge: @locationbase/bundles/*/@location
|
|
274
|
+
# then merge @locationbase/@location
|
|
275
|
+
#
|
|
276
|
+
|
|
277
|
+
# bundleDir = $cfg['location.bundles'] or 'bundles'
|
|
278
|
+
# bundleDir = 'bundles' if bundleDir.nil?
|
|
279
|
+
items = {}
|
|
280
|
+
# if (@locationbase + bundleDir).directory? then
|
|
281
|
+
# (@locationbase + bundleDir).children.sort.each do |bndl|
|
|
282
|
+
# if (bndl + @location).exist? then
|
|
283
|
+
# verbose("Loading from bundle #{bndl.basename}")
|
|
284
|
+
# bitems = localitems(bndl + @location)
|
|
285
|
+
# bitems.map!{|b| b[:bundled] = true; b} # mark items from bundles.
|
|
286
|
+
#
|
|
287
|
+
#
|
|
288
|
+
# # use synckey for quicker merging.
|
|
289
|
+
# bitems.each { |b| items[synckey(b)] = b }
|
|
290
|
+
# end
|
|
291
|
+
# end
|
|
292
|
+
# end
|
|
293
|
+
if location.exist? then
|
|
294
|
+
bitems = localitems(location)
|
|
295
|
+
# use synckey for quicker merging.
|
|
296
|
+
bitems.each { |b| items[synckey(b)] = b }
|
|
297
|
+
else
|
|
298
|
+
warning "Skipping missing location #{location}"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
items.values
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
##
|
|
305
|
+
# Get the full path for the local versions
|
|
306
|
+
def location
|
|
307
|
+
raise "Missing @project_section" if @project_section.nil?
|
|
308
|
+
Pathname.new($cfg['location.base']) + $project["#{@project_section}.location"]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
##
|
|
312
|
+
# Returns array of globs to search for files
|
|
313
|
+
def searchFor
|
|
314
|
+
raise "Missing @project_section" if @project_section.nil?
|
|
315
|
+
$project["#{@project_section}.include"]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
## Returns array of globs of files to ignore
|
|
319
|
+
def ignoring
|
|
320
|
+
raise "Missing @project_section" if @project_section.nil?
|
|
321
|
+
$project["#{@project_section}.exclude"]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
##
|
|
325
|
+
# Get a list of local items rooted at #from
|
|
326
|
+
#
|
|
327
|
+
# Children rarely need to override this. Only when the locallist is not a set
|
|
328
|
+
# of files in a directory will they need to override it.
|
|
329
|
+
#
|
|
330
|
+
# @param from Pathname: Directory of items to scan
|
|
331
|
+
# @return Array: of Hashes of item details
|
|
332
|
+
def localitems(from)
|
|
333
|
+
# TODO: Profile this.
|
|
334
|
+
debug "#{self.class.to_s}: Getting local items from: #{from}"
|
|
335
|
+
searchIn = from.to_s
|
|
336
|
+
sf = searchFor.map{|i| ::File.join(searchIn, i)}
|
|
337
|
+
debug "#{self.class.to_s}: Globs: #{sf}"
|
|
338
|
+
Dir[*sf].flatten.compact.reject do |p|
|
|
339
|
+
::File.directory?(p) or ignoring.any? do |i|
|
|
340
|
+
::File.fnmatch(i,p)
|
|
341
|
+
end
|
|
342
|
+
end.map do |path|
|
|
343
|
+
path = Pathname.new(path).realpath
|
|
344
|
+
item = toRemoteItem(from, path)
|
|
345
|
+
if item.kind_of?(Array) then
|
|
346
|
+
item.compact.map{|i| i[:local_path] = path; i}
|
|
347
|
+
elsif not item.nil? then
|
|
348
|
+
item[:local_path] = path
|
|
349
|
+
item
|
|
350
|
+
end
|
|
351
|
+
end.flatten.compact
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
#######################################################################
|
|
355
|
+
# Methods that provide the core status/syncup/syncdown
|
|
356
|
+
|
|
357
|
+
##
|
|
358
|
+
# Take a hash or something (a Commander::Command::Options) and return a hash
|
|
359
|
+
#
|
|
360
|
+
def elevate_hash(hsh)
|
|
361
|
+
# Commander::Command::Options stripped all of the methods from parent
|
|
362
|
+
# objects. I have not nice thoughts about that.
|
|
363
|
+
begin
|
|
364
|
+
hsh = hsh.__hash__
|
|
365
|
+
rescue NoMethodError
|
|
366
|
+
# swallow this.
|
|
367
|
+
end
|
|
368
|
+
# build a hash where the default is 'false' instead of 'nil'
|
|
369
|
+
Hash.new(false).merge(Hash.transform_keys_to_symbols(hsh))
|
|
370
|
+
end
|
|
371
|
+
private :elevate_hash
|
|
372
|
+
|
|
373
|
+
## Make things in Murano look like local project
|
|
374
|
+
#
|
|
375
|
+
# This creates, uploads, and deletes things as needed up in Murano to match
|
|
376
|
+
# what is in the local project directory.
|
|
377
|
+
def syncup(options={}, selected=[])
|
|
378
|
+
options = elevate_hash(options)
|
|
379
|
+
itemkey = @itemkey.to_sym
|
|
380
|
+
options[:asdown] = false
|
|
381
|
+
dt = status(options, selected)
|
|
382
|
+
toadd = dt[:toadd]
|
|
383
|
+
todel = dt[:todel]
|
|
384
|
+
tomod = dt[:tomod]
|
|
385
|
+
|
|
386
|
+
if options[:delete] then
|
|
387
|
+
todel.each do |item|
|
|
388
|
+
verbose "Removing item #{item[:synckey]}"
|
|
389
|
+
unless $cfg['tool.dry'] then
|
|
390
|
+
remove(item[itemkey])
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
if options[:create] then
|
|
395
|
+
toadd.each do |item|
|
|
396
|
+
verbose "Adding item #{item[:synckey]}"
|
|
397
|
+
unless $cfg['tool.dry'] then
|
|
398
|
+
upload(item[:local_path], item.reject{|k,v| k==:local_path}, false)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
if options[:update] then
|
|
403
|
+
tomod.each do |item|
|
|
404
|
+
verbose "Updating item #{item[:synckey]}"
|
|
405
|
+
unless $cfg['tool.dry'] then
|
|
406
|
+
upload(item[:local_path], item.reject{|k,v| k==:local_path}, true)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
## Make things in local project look like Murano
|
|
413
|
+
#
|
|
414
|
+
# This creates, downloads, and deletes things as needed up in the local project
|
|
415
|
+
# directory to match what is in Murano.
|
|
416
|
+
def syncdown(options={}, selected=[])
|
|
417
|
+
options = elevate_hash(options)
|
|
418
|
+
options[:asdown] = true
|
|
419
|
+
dt = status(options, selected)
|
|
420
|
+
into = location ###
|
|
421
|
+
toadd = dt[:toadd]
|
|
422
|
+
todel = dt[:todel]
|
|
423
|
+
tomod = dt[:tomod]
|
|
424
|
+
|
|
425
|
+
if options[:delete] then
|
|
426
|
+
todel.each do |item|
|
|
427
|
+
verbose "Removing item #{item[:synckey]}"
|
|
428
|
+
unless $cfg['tool.dry'] then
|
|
429
|
+
dest = tolocalpath(into, item)
|
|
430
|
+
removelocal(dest, item)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
if options[:create] then
|
|
435
|
+
toadd.each do |item|
|
|
436
|
+
verbose "Adding item #{item[:synckey]}"
|
|
437
|
+
unless $cfg['tool.dry'] then
|
|
438
|
+
dest = tolocalpath(into, item)
|
|
439
|
+
download(dest, item)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
if options[:update] then
|
|
444
|
+
tomod.each do |item|
|
|
445
|
+
verbose "Updating item #{item[:synckey]}"
|
|
446
|
+
unless $cfg['tool.dry'] then
|
|
447
|
+
dest = tolocalpath(into, item)
|
|
448
|
+
download(dest, item)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
## Call external diff tool on item
|
|
455
|
+
# WARNING: This will download the remote item to do the diff.
|
|
456
|
+
# @param item Hash: The item to get a diff of
|
|
457
|
+
# @return String: The diff output
|
|
458
|
+
def dodiff(item)
|
|
459
|
+
trmt = Tempfile.new([tolocalname(item, @itemkey)+'_remote_', '.lua'])
|
|
460
|
+
tlcl = Tempfile.new([tolocalname(item, @itemkey)+'_local_', '.lua'])
|
|
461
|
+
if item.has_key? :script then
|
|
462
|
+
Pathname.new(tlcl.path).open('wb') do |io|
|
|
463
|
+
io << item[:script]
|
|
464
|
+
end
|
|
465
|
+
else
|
|
466
|
+
Pathname.new(tlcl.path).open('wb') do |io|
|
|
467
|
+
io << item[:local_path].read
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
df = ""
|
|
471
|
+
begin
|
|
472
|
+
download(Pathname.new(trmt.path), item)
|
|
473
|
+
|
|
474
|
+
cmd = $cfg['diff.cmd'].shellsplit
|
|
475
|
+
cmd << trmt.path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR)
|
|
476
|
+
cmd << tlcl.path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR)
|
|
477
|
+
|
|
478
|
+
df, _ = Open3.capture2e(*cmd)
|
|
479
|
+
ensure
|
|
480
|
+
trmt.close
|
|
481
|
+
trmt.unlink
|
|
482
|
+
tlcl.close
|
|
483
|
+
tlcl.unlink
|
|
484
|
+
end
|
|
485
|
+
df
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
##
|
|
489
|
+
# Check if an item matches a pattern.
|
|
490
|
+
def _matcher(items, patterns)
|
|
491
|
+
items.map do |item|
|
|
492
|
+
if patterns.empty? then
|
|
493
|
+
item[:selected] = true
|
|
494
|
+
else
|
|
495
|
+
item[:selected] = patterns.any? do |pattern|
|
|
496
|
+
if pattern.to_s[0] == '#' then
|
|
497
|
+
match(item, pattern)
|
|
498
|
+
elsif not item.has_key? :local_path then
|
|
499
|
+
false
|
|
500
|
+
else
|
|
501
|
+
item[:local_path].fnmatch(pattern)
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
item
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
private :_matcher
|
|
509
|
+
|
|
510
|
+
## Get status of things here verses there
|
|
511
|
+
def status(options={}, selected=[])
|
|
512
|
+
options = elevate_hash(options)
|
|
513
|
+
itemkey = @itemkey.to_sym
|
|
514
|
+
|
|
515
|
+
there = _matcher(list(), selected)
|
|
516
|
+
here = _matcher(locallist(), selected)
|
|
517
|
+
|
|
518
|
+
therebox = {}
|
|
519
|
+
there.each do |item|
|
|
520
|
+
item = Hash.transform_keys_to_symbols(item)
|
|
521
|
+
item[:synckey] = synckey(item)
|
|
522
|
+
therebox[ item[:synckey] ] = item
|
|
523
|
+
end
|
|
524
|
+
herebox = {}
|
|
525
|
+
here.each do |item|
|
|
526
|
+
item = Hash.transform_keys_to_symbols(item)
|
|
527
|
+
item[:synckey] = synckey(item)
|
|
528
|
+
herebox[ item[:synckey] ] = item
|
|
529
|
+
end
|
|
530
|
+
toadd = []
|
|
531
|
+
todel = []
|
|
532
|
+
tomod = []
|
|
533
|
+
unchg = []
|
|
534
|
+
if options[:asdown] then
|
|
535
|
+
todel = (herebox.keys - therebox.keys).map{|key| herebox[key] }
|
|
536
|
+
toadd = (therebox.keys - herebox.keys).map{|key| therebox[key] }
|
|
537
|
+
else
|
|
538
|
+
toadd = (herebox.keys - therebox.keys).map{|key| herebox[key] }
|
|
539
|
+
todel = (therebox.keys - herebox.keys).map{|key| therebox[key] }
|
|
540
|
+
end
|
|
541
|
+
(herebox.keys & therebox.keys).each do |key|
|
|
542
|
+
# Want here to override there except for itemkey.
|
|
543
|
+
mrg = herebox[key].reject{|k,v| k==itemkey}
|
|
544
|
+
mrg = therebox[key].merge(mrg)
|
|
545
|
+
if docmp(herebox[key], therebox[key]) then
|
|
546
|
+
mrg[:diff] = dodiff(mrg) if options[:diff] and mrg[:selected]
|
|
547
|
+
tomod << mrg
|
|
548
|
+
else
|
|
549
|
+
unchg << mrg
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
if options[:unselected] then
|
|
553
|
+
{ :toadd=>toadd, :todel=>todel, :tomod=>tomod, :unchg=>unchg }
|
|
554
|
+
else
|
|
555
|
+
{
|
|
556
|
+
:toadd=>toadd.select{|i| i[:selected]}.map{|i| i.delete(:selected); i},
|
|
557
|
+
:todel=>todel.select{|i| i[:selected]}.map{|i| i.delete(:selected); i},
|
|
558
|
+
:tomod=>tomod.select{|i| i[:selected]}.map{|i| i.delete(:selected); i},
|
|
559
|
+
:unchg=>unchg.select{|i| i[:selected]}.map{|i| i.delete(:selected); i}
|
|
560
|
+
}
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
# vim: set ai et sw=2 ts=2 :
|