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.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +21 -0
  5. data/Gemfile +27 -0
  6. data/LICENSE.txt +19 -0
  7. data/MuranoCLI.gemspec +50 -0
  8. data/MuranoCLI.iss +50 -0
  9. data/README.markdown +208 -0
  10. data/Rakefile +188 -0
  11. data/TODO.taskpaper +122 -0
  12. data/bin/mr +8 -0
  13. data/bin/murano +84 -0
  14. data/docs/demo.md +109 -0
  15. data/lib/MrMurano/Account.rb +211 -0
  16. data/lib/MrMurano/Config-Migrate.rb +47 -0
  17. data/lib/MrMurano/Config.rb +286 -0
  18. data/lib/MrMurano/Mock.rb +63 -0
  19. data/lib/MrMurano/Product-1P-Device.rb +145 -0
  20. data/lib/MrMurano/Product-Resources.rb +195 -0
  21. data/lib/MrMurano/Product.rb +358 -0
  22. data/lib/MrMurano/ProjectFile.rb +349 -0
  23. data/lib/MrMurano/Solution-Cors.rb +46 -0
  24. data/lib/MrMurano/Solution-Endpoint.rb +177 -0
  25. data/lib/MrMurano/Solution-File.rb +150 -0
  26. data/lib/MrMurano/Solution-ServiceConfig.rb +140 -0
  27. data/lib/MrMurano/Solution-Services.rb +326 -0
  28. data/lib/MrMurano/Solution-Users.rb +129 -0
  29. data/lib/MrMurano/Solution.rb +59 -0
  30. data/lib/MrMurano/SubCmdGroupContext.rb +49 -0
  31. data/lib/MrMurano/SyncUpDown.rb +565 -0
  32. data/lib/MrMurano/commands/assign.rb +57 -0
  33. data/lib/MrMurano/commands/businessList.rb +45 -0
  34. data/lib/MrMurano/commands/completion.rb +152 -0
  35. data/lib/MrMurano/commands/config.rb +67 -0
  36. data/lib/MrMurano/commands/content.rb +130 -0
  37. data/lib/MrMurano/commands/cors.rb +30 -0
  38. data/lib/MrMurano/commands/domain.rb +17 -0
  39. data/lib/MrMurano/commands/gb.rb +33 -0
  40. data/lib/MrMurano/commands/init.rb +138 -0
  41. data/lib/MrMurano/commands/keystore.rb +157 -0
  42. data/lib/MrMurano/commands/logs.rb +78 -0
  43. data/lib/MrMurano/commands/mock.rb +63 -0
  44. data/lib/MrMurano/commands/password.rb +88 -0
  45. data/lib/MrMurano/commands/postgresql.rb +41 -0
  46. data/lib/MrMurano/commands/product.rb +14 -0
  47. data/lib/MrMurano/commands/productCreate.rb +39 -0
  48. data/lib/MrMurano/commands/productDelete.rb +33 -0
  49. data/lib/MrMurano/commands/productDevice.rb +84 -0
  50. data/lib/MrMurano/commands/productDeviceIdCmds.rb +86 -0
  51. data/lib/MrMurano/commands/productList.rb +45 -0
  52. data/lib/MrMurano/commands/productWrite.rb +27 -0
  53. data/lib/MrMurano/commands/show.rb +80 -0
  54. data/lib/MrMurano/commands/solution.rb +14 -0
  55. data/lib/MrMurano/commands/solutionCreate.rb +39 -0
  56. data/lib/MrMurano/commands/solutionDelete.rb +34 -0
  57. data/lib/MrMurano/commands/solutionList.rb +45 -0
  58. data/lib/MrMurano/commands/status.rb +92 -0
  59. data/lib/MrMurano/commands/sync.rb +60 -0
  60. data/lib/MrMurano/commands/timeseries.rb +115 -0
  61. data/lib/MrMurano/commands/tsdb.rb +271 -0
  62. data/lib/MrMurano/commands/usage.rb +23 -0
  63. data/lib/MrMurano/commands/zshcomplete.erb +112 -0
  64. data/lib/MrMurano/commands.rb +32 -0
  65. data/lib/MrMurano/hash.rb +20 -0
  66. data/lib/MrMurano/http.rb +153 -0
  67. data/lib/MrMurano/makePretty.rb +75 -0
  68. data/lib/MrMurano/schema/pf-v1.0.0.yaml +114 -0
  69. data/lib/MrMurano/schema/sf-v0.2.0.yaml +77 -0
  70. data/lib/MrMurano/schema/sf-v0.3.0.yaml +78 -0
  71. data/lib/MrMurano/template/mock.erb +9 -0
  72. data/lib/MrMurano/template/projectFile.murano.erb +81 -0
  73. data/lib/MrMurano/verbosing.rb +99 -0
  74. data/lib/MrMurano/version.rb +4 -0
  75. data/lib/MrMurano.rb +20 -0
  76. data/spec/Account-Passwords_spec.rb +242 -0
  77. data/spec/Account_spec.rb +272 -0
  78. data/spec/ConfigFile_spec.rb +50 -0
  79. data/spec/ConfigMigrate_spec.rb +89 -0
  80. data/spec/Config_spec.rb +409 -0
  81. data/spec/Http_spec.rb +204 -0
  82. data/spec/MakePretties_spec.rb +118 -0
  83. data/spec/Mock_spec.rb +53 -0
  84. data/spec/ProductBase_spec.rb +113 -0
  85. data/spec/ProductContent_spec.rb +162 -0
  86. data/spec/ProductResources_spec.rb +329 -0
  87. data/spec/Product_1P_Device_spec.rb +202 -0
  88. data/spec/Product_1P_RPC_spec.rb +175 -0
  89. data/spec/Product_spec.rb +153 -0
  90. data/spec/ProjectFile_spec.rb +324 -0
  91. data/spec/Solution-Cors_spec.rb +164 -0
  92. data/spec/Solution-Endpoint_spec.rb +581 -0
  93. data/spec/Solution-File_spec.rb +212 -0
  94. data/spec/Solution-ServiceConfig_spec.rb +202 -0
  95. data/spec/Solution-ServiceDevice_spec.rb +176 -0
  96. data/spec/Solution-ServiceEventHandler_spec.rb +385 -0
  97. data/spec/Solution-ServiceModules_spec.rb +465 -0
  98. data/spec/Solution-UsersRoles_spec.rb +207 -0
  99. data/spec/Solution_spec.rb +92 -0
  100. data/spec/SyncRoot_spec.rb +83 -0
  101. data/spec/SyncUpDown_spec.rb +495 -0
  102. data/spec/Verbosing_spec.rb +279 -0
  103. data/spec/_workspace.rb +27 -0
  104. data/spec/cmd_assign_spec.rb +51 -0
  105. data/spec/cmd_business_spec.rb +59 -0
  106. data/spec/cmd_common.rb +72 -0
  107. data/spec/cmd_config_spec.rb +68 -0
  108. data/spec/cmd_content_spec.rb +71 -0
  109. data/spec/cmd_cors_spec.rb +50 -0
  110. data/spec/cmd_device_spec.rb +96 -0
  111. data/spec/cmd_domain_spec.rb +32 -0
  112. data/spec/cmd_init_spec.rb +30 -0
  113. data/spec/cmd_keystore_spec.rb +97 -0
  114. data/spec/cmd_password_spec.rb +62 -0
  115. data/spec/cmd_status_spec.rb +239 -0
  116. data/spec/cmd_syncdown_spec.rb +86 -0
  117. data/spec/cmd_syncup_spec.rb +62 -0
  118. data/spec/cmd_usage_spec.rb +36 -0
  119. data/spec/fixtures/.mrmuranorc +9 -0
  120. data/spec/fixtures/ProjectFiles/invalid.yaml +9 -0
  121. data/spec/fixtures/ProjectFiles/only_meta.yaml +24 -0
  122. data/spec/fixtures/ProjectFiles/with_routes.yaml +27 -0
  123. data/spec/fixtures/SolutionFiles/0.2.0.json +20 -0
  124. data/spec/fixtures/SolutionFiles/0.2.0_invalid.json +18 -0
  125. data/spec/fixtures/SolutionFiles/0.2.json +21 -0
  126. data/spec/fixtures/SolutionFiles/0.3.0.json +20 -0
  127. data/spec/fixtures/SolutionFiles/0.3.0_invalid.json +19 -0
  128. data/spec/fixtures/SolutionFiles/0.3.json +20 -0
  129. data/spec/fixtures/SolutionFiles/basic.json +20 -0
  130. data/spec/fixtures/SolutionFiles/secret.json +6 -0
  131. data/spec/fixtures/configfile +9 -0
  132. data/spec/fixtures/dumped_config +42 -0
  133. data/spec/fixtures/mrmuranorc_deleted_bob +8 -0
  134. data/spec/fixtures/mrmuranorc_tool_bob +3 -0
  135. data/spec/fixtures/product_spec_files/example.exoline.spec.yaml +116 -0
  136. data/spec/fixtures/product_spec_files/example.murano.spec.yaml +14 -0
  137. data/spec/fixtures/product_spec_files/gwe.exoline.spec.yaml +21 -0
  138. data/spec/fixtures/product_spec_files/gwe.murano.spec.yaml +16 -0
  139. data/spec/fixtures/product_spec_files/lightbulb-no-state.yaml +11 -0
  140. data/spec/fixtures/product_spec_files/lightbulb.yaml +14 -0
  141. data/spec/fixtures/roles-three.yaml +11 -0
  142. data/spec/fixtures/syncable_content/assets/icon.png +0 -0
  143. data/spec/fixtures/syncable_content/assets/index.html +0 -0
  144. data/spec/fixtures/syncable_content/assets/js/script.js +0 -0
  145. data/spec/fixtures/syncable_content/modules/table_util.lua +58 -0
  146. data/spec/fixtures/syncable_content/routes/manyRoutes.lua +11 -0
  147. data/spec/fixtures/syncable_content/routes/singleRoute.lua +5 -0
  148. data/spec/fixtures/syncable_content/services/devdata.lua +18 -0
  149. data/spec/fixtures/syncable_content/services/timers.lua +4 -0
  150. data/spec/spec_helper.rb +119 -0
  151. 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 :