1
2
3 """ S3 Synchronization: Peer Repository Adapter
4
5 @copyright: 2014-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28 """
29
30 import sys
31 import urllib, urllib2
32
33 from gluon import *
34
35 from ..s3sync import S3SyncBaseAdapter
36
37
39 """
40 Mariner CommandBridge Synchronization Adapter
41
42 @status: experimental
43 """
44
45
47 """
48 Register at the repository (does nothing in CommandBridge)
49
50 @return: True if successful, otherwise False
51 """
52
53 return True
54
55
57 """
58 Login to the repository (does nothing in CommandBridge)
59
60 @return: None if successful, otherwise error message
61 """
62
63 return None
64
65
66 - def pull(self, task, onconflict=None):
67 """
68 Pull updates from this repository
69
70 @param task: the task Row
71 @param onconflict: synchronization conflict resolver
72 @return: tuple (error, mtime), with error=None if successful,
73 else error=message, and mtime=modification timestamp
74 of the youngest record received
75 """
76
77 error = "CommandBridge API pull not implemented"
78 current.log.error(error)
79 return (error, None)
80
81
82 - def push(self, task):
83 """
84 Push data for a task
85
86 @param task: the task Row
87 @return: tuple (error, mtime), with error=None if successful,
88 else error=message, and mtime=modification timestamp
89 of the youngest record sent
90 """
91
92 xml = current.xml
93 repository = self.repository
94
95 resource_name = task.resource_name
96 current.log.debug("S3SyncCommandBridge.push(%s, %s)" %
97 (repository.url, resource_name))
98
99
100 resource = current.s3db.resource(resource_name,
101 include_deleted=True)
102
103
104 folder = current.request.folder
105 import os
106 stylesheet = os.path.join(folder,
107 "static",
108 "formats",
109 "mcb",
110 "export.xsl")
111
112
113 last_push = task.last_push
114
115
116 filters = current.sync.get_filters(task.id)
117
118 settings = current.deployment_settings
119
120 identifiers = settings.get_sync_mcb_resource_identifiers()
121 resources = "".join("[%s:%s]" % (k, v) for k, v in identifiers.items())
122
123 identifiers = settings.get_sync_mcb_domain_identifiers()
124 domains = "".join("[%s:%s]" % (k, v) for k, v in identifiers.items())
125
126
127 data = resource.export_xml(filters = filters,
128 msince = last_push,
129 stylesheet = stylesheet,
130 pretty_print = True,
131 resources = resources,
132 domains = domains,
133 )
134
135 count = resource.results or 0
136 mtime = resource.muntil
137
138
139 remote = False
140 output = None
141 log = repository.log
142 if data and count:
143 response, message = self._send_request(method = "POST",
144 path = "BulkStream",
145 data = data,
146 )
147 if response is None:
148 result = log.FATAL
149 remote = True
150 if not message:
151 message = "unknown error"
152 output = message
153 else:
154 result = log.SUCCESS
155 message = "Data sent successfully (%s records)" % count
156 else:
157
158 result = log.WARNING
159 message = "No data to send"
160
161
162 log.write(repository_id = repository.id,
163 resource_name = resource_name,
164 transmission = log.OUT,
165 mode = log.PUSH,
166 action = "send",
167 remote = remote,
168 result = result,
169 message = message)
170
171 if output is not None:
172 mtime = None
173 return (output, mtime)
174
175
176
177
178 - def _send_request(self,
179 method="GET",
180 path=None,
181 args=None,
182 data=None,
183 auth=False):
184 """
185 Send a request to the CommandBridge API
186
187 @param method: the HTTP method
188 @param path: the path relative to the repository URL
189 @param data: the data to send
190 @param auth: this is an authorization request
191 """
192
193 xml = current.xml
194 repository = self.repository
195
196
197 url = repository.url.rstrip("/")
198 if path:
199 url = "/".join((url, path.lstrip("/")))
200 if args:
201 url = "?".join((url, urllib.urlencode(args)))
202
203
204 req = urllib2.Request(url=url)
205 handlers = []
206
207 site_key = repository.site_key
208 if not site_key:
209 message = "CommandBridge Authorization failed: no access token (site key)"
210 current.log.error(message)
211 return None, message
212 req.add_header("Authorization-Token", "%s" % site_key)
213
214
215 request_data = data if data is not None else ""
216 if request_data:
217 req.add_header("Content-Type", "application/xml")
218
219
220 req.add_header("Accept", "application/xml")
221
222
223 config = repository.config
224 proxy = repository.proxy or config.proxy or None
225 if proxy:
226 current.log.debug("using proxy=%s" % proxy)
227 proxy_handler = urllib2.ProxyHandler({"https": proxy})
228 handlers.append(proxy_handler)
229
230
231 if handlers:
232 opener = urllib2.build_opener(*handlers)
233 urllib2.install_opener(opener)
234
235
236 response = None
237 message = None
238 try:
239 if method == "POST":
240 f = urllib2.urlopen(req, data=request_data)
241 else:
242 f = urllib2.urlopen(req)
243 except urllib2.HTTPError, e:
244 message = "HTTP %s: %s" % (e.code, e.reason)
245
246 error_response = xml.parse(e)
247 if error_response:
248 error_messages = error_response.findall("Message")
249 details = " / ".join(item.text for item in error_messages)
250 message = "%s (%s)" % (message, details)
251 else:
252 response = xml.parse(f)
253 if response is None:
254 if method == "POST":
255 response = True
256 elif xml.error:
257 message = xml.error
258
259 return response, message
260
261
262