MuranoCLI 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 :