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,150 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require "http/form_data"
|
|
4
|
+
require 'digest/sha1'
|
|
5
|
+
require 'mime/types'
|
|
6
|
+
require 'pp'
|
|
7
|
+
require 'MrMurano/Solution'
|
|
8
|
+
|
|
9
|
+
module MrMurano
|
|
10
|
+
# …/file
|
|
11
|
+
class File < SolutionBase
|
|
12
|
+
def initialize
|
|
13
|
+
super
|
|
14
|
+
@uriparts << 'file'
|
|
15
|
+
@itemkey = :path
|
|
16
|
+
@project_section = :assets
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Get a list of all of the static content
|
|
21
|
+
def list
|
|
22
|
+
get()
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Get one item of the static content.
|
|
27
|
+
def fetch(path, &block)
|
|
28
|
+
get(path) do |request, http|
|
|
29
|
+
http.request(request) do |resp|
|
|
30
|
+
case resp
|
|
31
|
+
when Net::HTTPSuccess
|
|
32
|
+
if block_given? then
|
|
33
|
+
resp.read_body(&block)
|
|
34
|
+
else
|
|
35
|
+
resp.read_body do |chunk|
|
|
36
|
+
$stdout.write chunk
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
showHttpError(request, resp)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Delete a file
|
|
49
|
+
def remove(path)
|
|
50
|
+
# TODO test
|
|
51
|
+
delete('/'+path)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def curldebug(request)
|
|
55
|
+
# The upload will get printed out inside of upload.
|
|
56
|
+
# Because we don't have the correct info here.
|
|
57
|
+
if request.method != 'PUT' then
|
|
58
|
+
super(request)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Upload a file
|
|
64
|
+
# @param modify Bool: True if item exists already and this is changing it
|
|
65
|
+
def upload(local, remote, modify)
|
|
66
|
+
local = Pathname.new(local) unless local.kind_of? Pathname
|
|
67
|
+
|
|
68
|
+
uri = endPoint('upload' + remote[:path])
|
|
69
|
+
# kludge past for a bit.
|
|
70
|
+
#`curl -s -H 'Authorization: token #{@token}' '#{uri.to_s}' -F file=@#{local.to_s}`
|
|
71
|
+
|
|
72
|
+
# http://stackoverflow.com/questions/184178/ruby-how-to-post-a-file-via-http-as-multipart-form-data
|
|
73
|
+
#
|
|
74
|
+
# Look at: https://github.com/httprb/http
|
|
75
|
+
# If it works well, consider porting over to it.
|
|
76
|
+
#
|
|
77
|
+
# Or just: https://github.com/httprb/form_data.rb ?
|
|
78
|
+
#
|
|
79
|
+
# Most of these pull into ram. So maybe just go with that. Would guess that
|
|
80
|
+
# truely large static content is rare, and we can optimize/fix that later.
|
|
81
|
+
|
|
82
|
+
file = HTTP::FormData::File.new(local.to_s, {:mime_type=>remote[:mime_type]})
|
|
83
|
+
form = HTTP::FormData.create(:file=>file)
|
|
84
|
+
req = Net::HTTP::Put.new(uri)
|
|
85
|
+
set_def_headers(req)
|
|
86
|
+
workit(req) do |request,http|
|
|
87
|
+
request.content_type = form.content_type
|
|
88
|
+
request.content_length = form.content_length
|
|
89
|
+
request.body = form.to_s
|
|
90
|
+
|
|
91
|
+
if $cfg['tool.curldebug'] then
|
|
92
|
+
a = []
|
|
93
|
+
a << %{curl -s -H 'Authorization: #{request['authorization']}'}
|
|
94
|
+
a << %{-H 'User-Agent: #{request['User-Agent']}'}
|
|
95
|
+
a << %{-X #{request.method}}
|
|
96
|
+
a << %{'#{request.uri.to_s}'}
|
|
97
|
+
a << %{-F file=@#{local.to_s}}
|
|
98
|
+
puts a.join(' ')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
response = http.request(request)
|
|
102
|
+
case response
|
|
103
|
+
when Net::HTTPSuccess
|
|
104
|
+
else
|
|
105
|
+
showHttpError(request, response)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def tolocalname(item, key)
|
|
111
|
+
name = item[key]
|
|
112
|
+
name = $cfg['files.default_page'] if name == '/'
|
|
113
|
+
name
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def toRemoteItem(from, path)
|
|
117
|
+
item = super(from, path)
|
|
118
|
+
name = item[:name]
|
|
119
|
+
name = '/' if name == $cfg['files.default_page']
|
|
120
|
+
name = "/#{name}" unless name.chars.first == '/'
|
|
121
|
+
|
|
122
|
+
mime = MIME::Types.type_for(path.to_s)[0] || MIME::Types["application/octet-stream"][0]
|
|
123
|
+
|
|
124
|
+
# It does not actually take the SHA1 of the file.
|
|
125
|
+
# It first converts the file to hex, then takes the SHA1 of that string
|
|
126
|
+
#sha1 = Digest::SHA1.file(path.to_s).hexdigest
|
|
127
|
+
sha1 = Digest::SHA1.new
|
|
128
|
+
path.open('rb:ASCII-8BIT') do |io|
|
|
129
|
+
while chunk = io.read(1048576) do
|
|
130
|
+
sha1 << Digest.hexencode(chunk)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
debug "Checking #{name} (#{mime.simplified} #{sha1.hexdigest})"
|
|
134
|
+
|
|
135
|
+
{:path=>name, :mime_type=>mime.simplified, :checksum=>sha1.hexdigest}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def synckey(item)
|
|
139
|
+
item[:path]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def docmp(itemA, itemB)
|
|
143
|
+
return (itemA[:mime_type] != itemB[:mime_type] or
|
|
144
|
+
itemA[:checksum] != itemB[:checksum])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
end
|
|
148
|
+
SyncRoot.add('files', File, 'S', %{Static Files}, true)
|
|
149
|
+
end
|
|
150
|
+
# vim: set ai et sw=2 ts=2 :
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'MrMurano/Solution'
|
|
2
|
+
|
|
3
|
+
module MrMurano
|
|
4
|
+
# …/serviceconfig
|
|
5
|
+
class ServiceConfig < SolutionBase
|
|
6
|
+
def initialize
|
|
7
|
+
super
|
|
8
|
+
@uriparts << 'serviceconfig'
|
|
9
|
+
@scid = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def list
|
|
13
|
+
get()[:items]
|
|
14
|
+
end
|
|
15
|
+
def fetch(id)
|
|
16
|
+
get('/' + id.to_s)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def scid_for_name(name)
|
|
20
|
+
name = name.to_s unless name.kind_of? String
|
|
21
|
+
scr = list().select{|i| i[:service] == name}.first
|
|
22
|
+
return nil if scr.nil?
|
|
23
|
+
scr[:id]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def scid
|
|
27
|
+
return @scid unless @scid.nil?
|
|
28
|
+
@scid = scid_for_name(@serviceName)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def info(id=scid)
|
|
32
|
+
get("/#{id}/info")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def logs(id=scid)
|
|
36
|
+
get("/#{id}/logs")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(opid, meth=:get, data=nil, id=scid, &block)
|
|
40
|
+
raise "Service '#{@serviceName}' not enabled for this Solution" if id.nil?
|
|
41
|
+
call = "/#{id.to_s}/call/#{opid.to_s}"
|
|
42
|
+
debug "Will call: #{call}"
|
|
43
|
+
case meth
|
|
44
|
+
when :get
|
|
45
|
+
get(call, data, &block)
|
|
46
|
+
when :post
|
|
47
|
+
data = {} if data.nil?
|
|
48
|
+
post(call, data, &block)
|
|
49
|
+
when :put
|
|
50
|
+
data = {} if data.nil?
|
|
51
|
+
put(call, data, &block)
|
|
52
|
+
when :delete
|
|
53
|
+
delete(call, &block)
|
|
54
|
+
else
|
|
55
|
+
raise "Unknown method: #{meth}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
## This is only used for debugging and deciphering APIs.
|
|
62
|
+
#
|
|
63
|
+
# There was once a plan for using this to automagically map commands into
|
|
64
|
+
# services by reading their schema. That plan had too much magic and was too
|
|
65
|
+
# fragile for real use.
|
|
66
|
+
#
|
|
67
|
+
# A much better UI/UX happens with human intervention.
|
|
68
|
+
# :nocov:
|
|
69
|
+
class Services < SolutionBase
|
|
70
|
+
def initialize
|
|
71
|
+
super
|
|
72
|
+
@uriparts << 'service'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sid_for_name(name)
|
|
76
|
+
name = name.to_s unless name.kind_of? String
|
|
77
|
+
scr = list().select{|i| i[:alias] == name}.first
|
|
78
|
+
scr[:id]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def sid
|
|
82
|
+
return @sid unless @sid.nil?
|
|
83
|
+
@sid = sid_for_name(@serviceName)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def list
|
|
87
|
+
ret = get()
|
|
88
|
+
ret[:items]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def schema(id=sid)
|
|
92
|
+
# TODO: cache schema in user dir?
|
|
93
|
+
get("/#{id}/schema")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
## Get list of call operations from a schema
|
|
97
|
+
def callable(id=sid)
|
|
98
|
+
scm = schema(id)
|
|
99
|
+
calls = []
|
|
100
|
+
scm[:paths].each do |path, methods|
|
|
101
|
+
methods.each do |method, params|
|
|
102
|
+
if params.kind_of?(Hash) and
|
|
103
|
+
not params['x-internal-use'.to_sym] and
|
|
104
|
+
params.has_key?(:operationId) then
|
|
105
|
+
calls << [method, params[:operationId]]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
calls
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
# :nocov:
|
|
113
|
+
|
|
114
|
+
# Device config interface for the assign commands.
|
|
115
|
+
class SC_Device < ServiceConfig
|
|
116
|
+
def initialize
|
|
117
|
+
super
|
|
118
|
+
@serviceName = 'device'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def assignTriggers(products)
|
|
122
|
+
details = fetch(scid)
|
|
123
|
+
products = [products] unless products.kind_of? Array
|
|
124
|
+
details[:triggers] = {:pid=>products}
|
|
125
|
+
details[:parameters] = {:pid=>products}
|
|
126
|
+
|
|
127
|
+
put('/'+scid, details)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def showTriggers
|
|
131
|
+
details = fetch(scid)
|
|
132
|
+
|
|
133
|
+
return [] if details[:triggers].nil?
|
|
134
|
+
details[:triggers][:pid]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# vim: set ai et sw=2 ts=2 :
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require 'date'
|
|
7
|
+
require 'digest/sha1'
|
|
8
|
+
require 'MrMurano/Solution'
|
|
9
|
+
|
|
10
|
+
module MrMurano
|
|
11
|
+
##
|
|
12
|
+
# Things that servers do that is common.
|
|
13
|
+
class ServiceBase < SolutionBase
|
|
14
|
+
|
|
15
|
+
def mkalias(remote)
|
|
16
|
+
# :nocov:
|
|
17
|
+
raise "Needs to be implemented in child"
|
|
18
|
+
# :nocov:
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mkname(remote)
|
|
22
|
+
# :nocov:
|
|
23
|
+
raise "Needs to be implemented in child"
|
|
24
|
+
# :nocov:
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def list
|
|
28
|
+
ret = get()
|
|
29
|
+
ret[:items]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch(name)
|
|
33
|
+
raise "Missing name!" if name.nil?
|
|
34
|
+
raise "Empty name!" if name.empty?
|
|
35
|
+
ret = get('/'+CGI.escape(name))
|
|
36
|
+
error "Unexpected result type, assuming empty instead: #{ret}" unless ret.kind_of? Hash
|
|
37
|
+
ret = {} unless ret.kind_of? Hash
|
|
38
|
+
if block_given? then
|
|
39
|
+
yield (ret[:script] or '')
|
|
40
|
+
else
|
|
41
|
+
ret[:script] or ''
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ??? remove
|
|
46
|
+
def remove(name)
|
|
47
|
+
delete('/'+name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param modify Bool: True if item exists already and this is changing it
|
|
51
|
+
def upload(local, remote, modify=false)
|
|
52
|
+
local = Pathname.new(local) unless local.kind_of? Pathname
|
|
53
|
+
raise "no file" unless local.exist?
|
|
54
|
+
|
|
55
|
+
# we assume these are small enough to slurp.
|
|
56
|
+
script = local.read
|
|
57
|
+
|
|
58
|
+
pst = remote.merge ({
|
|
59
|
+
:solution_id => $cfg['solution.id'],
|
|
60
|
+
:script => script,
|
|
61
|
+
:alias=>mkalias(remote),
|
|
62
|
+
:name=>mkname(remote),
|
|
63
|
+
})
|
|
64
|
+
debug "f: #{local} >> #{pst.reject{|k,_| k==:script}.to_json}"
|
|
65
|
+
# try put, if 404, then post.
|
|
66
|
+
put('/'+mkalias(remote), pst) do |request, http|
|
|
67
|
+
response = http.request(request)
|
|
68
|
+
case response
|
|
69
|
+
when Net::HTTPSuccess
|
|
70
|
+
#return JSON.parse(response.body)
|
|
71
|
+
when Net::HTTPNotFound
|
|
72
|
+
verbose "Doesn't exist, creating"
|
|
73
|
+
post('/', pst)
|
|
74
|
+
else
|
|
75
|
+
showHttpError(request, response)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
cacheUpdateTimeFor(local)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def docmp(itemA, itemB)
|
|
82
|
+
if itemA[:updated_at].nil? and itemA[:local_path] then
|
|
83
|
+
ct = cachedUpdateTimeFor(itemA[:local_path])
|
|
84
|
+
itemA[:updated_at] = ct unless ct.nil?
|
|
85
|
+
itemA[:updated_at] = itemA[:local_path].mtime.getutc if ct.nil?
|
|
86
|
+
elsif itemA[:updated_at].kind_of? String then
|
|
87
|
+
itemA[:updated_at] = DateTime.parse(itemA[:updated_at]).to_time.getutc
|
|
88
|
+
end
|
|
89
|
+
if itemB[:updated_at].nil? and itemB[:local_path] then
|
|
90
|
+
ct = cachedUpdateTimeFor(itemB[:local_path])
|
|
91
|
+
itemB[:updated_at] = ct unless ct.nil?
|
|
92
|
+
itemB[:updated_at] = itemB[:local_path].mtime.getutc if ct.nil?
|
|
93
|
+
elsif itemB[:updated_at].kind_of? String then
|
|
94
|
+
itemB[:updated_at] = DateTime.parse(itemB[:updated_at]).to_time.getutc
|
|
95
|
+
end
|
|
96
|
+
return itemA[:updated_at].to_time.round != itemB[:updated_at].to_time.round
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def cacheFileName
|
|
100
|
+
['cache',
|
|
101
|
+
self.class.to_s.gsub(/\W+/,'_'),
|
|
102
|
+
@sid,
|
|
103
|
+
'yaml'].join('.')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def cacheUpdateTimeFor(local_path, time=nil)
|
|
107
|
+
time = Time.now.getutc if time.nil?
|
|
108
|
+
entry = {
|
|
109
|
+
:sha1=>Digest::SHA1.file(local_path.to_s).hexdigest,
|
|
110
|
+
:updated_at=>time.to_datetime.iso8601(3)
|
|
111
|
+
}
|
|
112
|
+
cacheFile = $cfg.file_at(cacheFileName)
|
|
113
|
+
if cacheFile.file? then
|
|
114
|
+
cacheFile.open('r+') do |io|
|
|
115
|
+
cache = YAML.load(io)
|
|
116
|
+
cache = {} unless cache
|
|
117
|
+
io.rewind
|
|
118
|
+
cache[local_path.to_s] = entry
|
|
119
|
+
io << cache.to_yaml
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
cacheFile.open('w') do |io|
|
|
123
|
+
cache = {}
|
|
124
|
+
cache[local_path.to_s] = entry
|
|
125
|
+
io << cache.to_yaml
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
time
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def cachedUpdateTimeFor(local_path)
|
|
132
|
+
cksm = Digest::SHA1.file(local_path.to_s).hexdigest
|
|
133
|
+
cacheFile = $cfg.file_at(cacheFileName)
|
|
134
|
+
return nil unless cacheFile.file?
|
|
135
|
+
ret = nil
|
|
136
|
+
cacheFile.open('r') do |io|
|
|
137
|
+
cache = YAML.load(io)
|
|
138
|
+
return nil unless cache
|
|
139
|
+
if cache.has_key?(local_path.to_s) then
|
|
140
|
+
entry = cache[local_path.to_s]
|
|
141
|
+
debug("For #{local_path}:")
|
|
142
|
+
debug(" cached: #{entry.to_s}")
|
|
143
|
+
debug(" cm: #{cksm}")
|
|
144
|
+
if entry.kind_of?(Hash) then
|
|
145
|
+
if entry[:sha1] == cksm and entry.has_key?(:updated_at) then
|
|
146
|
+
ret = DateTime.parse(entry[:updated_at])
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
ret
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# …/library
|
|
156
|
+
class Library < ServiceBase
|
|
157
|
+
def initialize
|
|
158
|
+
super
|
|
159
|
+
@uriparts << 'library'
|
|
160
|
+
@itemkey = :alias
|
|
161
|
+
@project_section = :modules
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def tolocalname(item, key)
|
|
165
|
+
name = item[:name]
|
|
166
|
+
"#{name}.lua"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def mkalias(remote)
|
|
170
|
+
if remote.has_key? :name then
|
|
171
|
+
[$cfg['solution.id'], remote[:name]].join('_')
|
|
172
|
+
else
|
|
173
|
+
raise "Missing parts! #{remote.to_json}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def mkname(remote)
|
|
178
|
+
if remote.has_key? :name then
|
|
179
|
+
remote[:name]
|
|
180
|
+
else
|
|
181
|
+
raise "Missing parts! #{remote.to_json}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def toRemoteItem(from, path)
|
|
186
|
+
name = path.basename.to_s.sub(/\..*/, '')
|
|
187
|
+
{:name => name}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def synckey(item)
|
|
191
|
+
item[:name]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
SyncRoot.add('modules', Library, 'M', %{Modules}, true)
|
|
195
|
+
|
|
196
|
+
# …/eventhandler
|
|
197
|
+
class EventHandler < ServiceBase
|
|
198
|
+
def initialize
|
|
199
|
+
super
|
|
200
|
+
@uriparts << 'eventhandler'
|
|
201
|
+
@itemkey = :alias
|
|
202
|
+
@project_section = :services
|
|
203
|
+
@match_header = /--#EVENT (?<service>\S+) (?<event>\S+)/
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def mkalias(remote)
|
|
207
|
+
if remote.has_key? :service and remote.has_key? :event then
|
|
208
|
+
[$cfg['solution.id'], remote[:service], remote[:event]].join('_')
|
|
209
|
+
else
|
|
210
|
+
raise "Missing parts! #{remote.to_json}"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def mkname(remote)
|
|
215
|
+
if remote.has_key? :service and remote.has_key? :event then
|
|
216
|
+
[remote[:service], remote[:event]].join('_')
|
|
217
|
+
else
|
|
218
|
+
raise "Missing parts! #{remote.to_json}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def list
|
|
223
|
+
ret = get()
|
|
224
|
+
# eventhandler.skiplist is a list of whitespace seperated dot-paired values.
|
|
225
|
+
# fe: service.event service service service.event
|
|
226
|
+
skiplist = ($cfg['eventhandler.skiplist'] or '').split
|
|
227
|
+
ret[:items].reject { |i|
|
|
228
|
+
i.has_key?(:service) and i.has_key?(:event) and
|
|
229
|
+
( skiplist.include? i[:service] or
|
|
230
|
+
skiplist.include? "#{i[:service]}.#{i[:event]}"
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def fetch(name)
|
|
236
|
+
ret = get('/'+CGI.escape(name))
|
|
237
|
+
if ret.nil? then
|
|
238
|
+
error "Fetch for #{name} returned nil; skipping"
|
|
239
|
+
return ''
|
|
240
|
+
end
|
|
241
|
+
aheader = (ret[:script].lines.first or "").chomp
|
|
242
|
+
dheader = "--#EVENT #{ret[:service]} #{ret[:event]}"
|
|
243
|
+
if block_given? then
|
|
244
|
+
yield dheader + "\n" if aheader != dheader
|
|
245
|
+
yield ret[:script]
|
|
246
|
+
else
|
|
247
|
+
res = ''
|
|
248
|
+
res << dheader + "\n" if aheader != dheader
|
|
249
|
+
res << ret[:script]
|
|
250
|
+
res
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def tolocalname(item, key)
|
|
255
|
+
"#{item[:name]}.lua"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def toRemoteItem(from, path)
|
|
259
|
+
# This allows multiple events to be in the same file. This is a lie.
|
|
260
|
+
# This only finds the last event in a file.
|
|
261
|
+
# :legacy support doesn't allow for that. but that's ok.
|
|
262
|
+
path = Pathname.new(path) unless path.kind_of? Pathname
|
|
263
|
+
cur = nil
|
|
264
|
+
lineno=0
|
|
265
|
+
path.readlines().each do |line|
|
|
266
|
+
md = @match_header.match(line)
|
|
267
|
+
if not md.nil? then
|
|
268
|
+
# header line.
|
|
269
|
+
cur = {:service=>md[:service],
|
|
270
|
+
:event=>md[:event],
|
|
271
|
+
:local_path=>path,
|
|
272
|
+
:line=>lineno,
|
|
273
|
+
:script=>line}
|
|
274
|
+
elsif not cur.nil? and not cur[:script].nil? then
|
|
275
|
+
cur[:script] << line
|
|
276
|
+
end
|
|
277
|
+
lineno += 1
|
|
278
|
+
end
|
|
279
|
+
cur[:line_end] = lineno unless cur.nil?
|
|
280
|
+
|
|
281
|
+
# If cur is nil here, then we need to do a :legacy check.
|
|
282
|
+
if cur.nil? and $project['services.legacy'].kind_of? Hash then
|
|
283
|
+
spath = path.relative_path_from(from)
|
|
284
|
+
debug "No headers: #{spath}"
|
|
285
|
+
service, event = $project['services.legacy'][spath.to_s]
|
|
286
|
+
debug "Legacy lookup #{spath} => [#{service}, #{event}]"
|
|
287
|
+
unless service.nil? or event.nil? then
|
|
288
|
+
warning "Event in #{spath} missing header, but has legacy support."
|
|
289
|
+
warning "Please add the header \"--#EVENT #{service} #{event}\""
|
|
290
|
+
cur = {:service=>service,
|
|
291
|
+
:event=>event,
|
|
292
|
+
:local_path=>path,
|
|
293
|
+
:line=>0,
|
|
294
|
+
:line_end => lineno,
|
|
295
|
+
:script=>path.read()} # FIXME: ick, fix this.
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
cur
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def match(item, pattern)
|
|
302
|
+
# Pattern is: #{service}#{event}
|
|
303
|
+
pattern_pattern = /^#(?<service>[^#]*)#(?<event>.*)/i
|
|
304
|
+
md = pattern_pattern.match(pattern)
|
|
305
|
+
return false if md.nil?
|
|
306
|
+
debug "match pattern: '#{md[:service]}' '#{md[:event]}'"
|
|
307
|
+
|
|
308
|
+
unless md[:service].empty? then
|
|
309
|
+
return false unless item[:service].downcase == md[:service].downcase
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
unless md[:event].empty? then
|
|
313
|
+
return false unless item[:event].downcase == md[:event].downcase
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
true # Both match (or are empty.)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def synckey(item)
|
|
320
|
+
"#{item[:service]}_#{item[:event]}"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
SyncRoot.add('eventhandlers', EventHandler, 'E', %{Event Handlers}, true)
|
|
324
|
+
|
|
325
|
+
end
|
|
326
|
+
# vim: set ai et sw=2 ts=2 :
|