]> git.ipfire.org Git - pakfire.git/blame - python/pakfire/transport.py
Check if there is a progressbar, when calling finish.
[pakfire.git] / python / pakfire / transport.py
CommitLineData
aa14071d
MT
1#!/usr/bin/python
2###############################################################################
3# #
4# Pakfire - The IPFire package management system #
5# Copyright (C) 2013 Pakfire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
21
22from __future__ import division
23
24import base64
25import hashlib
26import json
27import os
28import time
29import urlgrabber
30import urllib
31import urlparse
32
33import pakfire.downloader
34import pakfire.util
35
36from pakfire.constants import *
37from pakfire.i18n import _
38
39import logging
40log = logging.getLogger("pakfire.transport")
41
42
43class PakfireHubTransportUploader(object):
44 """
45 Handles the upload of a single file to the hub.
46 """
47
48 def __init__(self, transport, filename):
49 self.transport = transport
50 self.filename = filename
51
52 def get_upload_id(self):
53 """
54 Gets an upload from the pakfire hub.
55 """
56 # Calculate the SHA1 sum of the file to upload.
57 h = hashlib.new("sha1")
58 with open(self.filename, "rb") as f:
59 while True:
60 buf = f.read(CHUNK_SIZE)
61 if not buf:
62 break
63
64 h.update(buf)
65
66 data = {
67 "filename" : os.path.basename(self.filename),
68 "filesize" : os.path.getsize(self.filename),
69 "hash" : h.hexdigest(),
70 }
71
72 upload_id = self.transport.get("/uploads/create", data=data)
73 log.debug("Got upload id: %s" % upload_id)
74
75 return upload_id
76
77 def send_file(self, upload_id, progress_callback=None):
78 """
79 Sends the file content to the server.
80
81 The data is splitted into chunks, which are
82 sent one after an other.
83 """
84 with open(self.filename, "rb") as f:
85 # Initial chunk size.
86 chunk_size = CHUNK_SIZE
87
88 # Count the already transmitted bytes.
89 transferred = 0
90
91 while True:
92 chunk = f.read(chunk_size)
93 if not chunk:
94 break
95
96 log.debug("Got chunk of %s bytes" % len(chunk))
97
98 # Save the time when we started to send this bit.
99 time_started = time.time()
100
101 # Send the chunk to the server.
102 self.send_chunk(upload_id, chunk)
103
104 # Save the duration.time after the chunk has been transmitted
105 # and adjust chunk size to send one chunk per second.
106 duration = time.time() - time_started
107 chunk_size = int(chunk_size / duration)
108
109 # Never let chunk_size drop under CHUNK_SIZE:
110 if chunk_size < CHUNK_SIZE:
111 chunk_size = CHUNK_SIZE
112
113 # Add up the send amount of data.
114 transferred += len(chunk)
115 if progress_callback:
116 progress_callback(transferred)
117
118 def send_chunk(self, upload_id, data):
119 """
120 Sends a piece of the file to the server.
121 """
122 # Calculate checksum over the chunk data.
123 h = hashlib.new("sha512")
124 h.update(data)
125 chksum = h.hexdigest()
126
127 # Encode data in base64.
128 data = base64.b64encode(data)
129
130 # Send chunk data to the server.
131 self.transport.post("/uploads/%s/sendchunk" % upload_id,
132 data={ "chksum" : chksum, "data" : data })
133
134 def destroy_upload(self, upload_id):
135 """
136 Destroys the upload on the server.
137 """
138 self.transport.get("/uploads/%s/destroy" % upload_id)
139
140 def finish_upload(self, upload_id):
141 """
142 Signals to the server, that the upload has finished.
143 """
144 self.transport.get("/uploads/%s/finished" % upload_id)
145
146 def run(self):
147 upload_id = None
148
149 # Create a progress bar.
150 progress = pakfire.util.make_progress(
151 os.path.basename(self.filename), os.path.getsize(self.filename), speed=True, eta=True,
152 )
153
154 try:
155 # Get an upload ID.
156 upload_id = self.get_upload_id()
157
158 # Send the file content.
159 self.send_file(upload_id, progress_callback=progress.update)
160
161 except:
92470f48
MT
162 if progress:
163 progress.finish()
aa14071d
MT
164
165 # Remove broken upload from server.
166 if upload_id:
167 self.destroy_upload(upload_id)
168
169 # XXX catch fatal errors
170 raise
171
172 else:
92470f48
MT
173 if progress:
174 progress.finish()
aa14071d
MT
175
176 # If no exception was raised, the upload
177 # has finished.
178 self.finish_upload(upload_id)
179
180 # Return the upload id so some code can actually do something
181 # with the file on the server.
182 return upload_id
183
184
185class PakfireHubTransport(object):
186 """
187 Connection to the pakfire hub.
188 """
189
190 def __init__(self, config):
191 self.config = config
192
193 # Create connection to the hub.
194 self.grabber = pakfire.downloader.PakfireGrabber(
195 self.config, prefix=self.url,
196 )
197
aa14071d
MT
198 @property
199 def url(self):
200 """
201 Construct a right URL out of the given
202 server, username and password.
203
204 Basicly this just adds the credentials
205 to the URL.
206 """
207 # Get credentials.
208 server, username, password = self.config.get_hub_credentials()
209
210 # Parse the given URL.
211 url = urlparse.urlparse(server)
212 assert url.scheme in ("http", "https")
213
214 # Build new URL.
215 ret = "%s://" % url.scheme
216
217 # Add credentials if provided.
218 if username and password:
219 ret += "%s:%s@" % (username, password)
220
221 # Add path components.
222 ret += url.netloc
223
224 return ret
225
226 def one_request(self, url, **kwargs):
227 try:
228 return self.grabber.urlread(url, **kwargs)
229
230 except urlgrabber.grabber.URLGrabError, e:
231 # Timeout
232 if e.errno == 12:
233 raise TransportConnectionTimeoutError, e
234
235 # Handle common HTTP errors
236 elif e.errno == 14:
237 # Connection errors
238 if e.code == 5:
239 raise TransportConnectionProxyError, url
240 elif e.code == 6:
241 raise TransportConnectionDNSError, url
242 elif e.code == 7:
243 raise TransportConnectionResetError, url
244 elif e.code == 23:
245 raise TransportConnectionWriteError, url
246 elif e.code == 26:
247 raise TransportConnectionReadError, url
248
249 # SSL errors
250 elif e.code == 52:
251 raise TransportSSLCertificateExpiredError, url
252
253 # HTTP error codes
254 elif e.code == 403:
255 raise TransportForbiddenError, url
256 elif e.code == 404:
257 raise TransportNotFoundError, url
258 elif e.code == 500:
259 raise TransportInternalServerError, url
6a05651d
MT
260 elif e.code == 504:
261 raise TransportConnectionTimeoutError, url
aa14071d
MT
262
263 # All other exceptions...
264 raise
265
266 def request(self, url, tries=None, **kwargs):
267 # tries = None implies wait infinitely
268
269 while tries or tries is None:
270 if tries:
271 tries -= 1
272
273 try:
274 return self.one_request(url, **kwargs)
275
276 # 500 - Internal Server Error
277 except TransportInternalServerError, e:
278 log.exception("%s" % e.__class__.__name__)
279
280 # Wait a minute before trying again.
281 time.sleep(60)
282
283 # Retry on connection problems.
284 except TransportConnectionError, e:
285 log.exception("%s" % e.__class__.__name__)
286
287 # Wait for 10 seconds.
288 time.sleep(10)
289
290 raise TransportMaxTriesExceededError
291
292 def escape_args(self, **kwargs):
293 return urllib.urlencode(kwargs)
294
295 def get(self, url, data={}, **kwargs):
296 """
297 Sends a HTTP GET request to the given URL.
298
299 All given keyword arguments are considered as form data.
300 """
301 params = self.escape_args(**data)
302
303 if params:
304 url = "%s?%s" % (url, params)
305
306 return self.request(url, **kwargs)
307
308 def post(self, url, data={}, **kwargs):
309 """
310 Sends a HTTP POST request to the given URL.
311
312 All keyword arguments are considered as form data.
313 """
314 params = self.escape_args(**data)
315 if params:
316 kwargs.update({
317 "data" : params,
318 })
319
320 return self.request(url, **kwargs)
321
322 def upload_file(self, filename):
323 """
324 Uploads the given file to the server.
325 """
326 uploader = PakfireHubTransportUploader(self, filename)
327 upload_id = uploader.run()
328
329 return upload_id
330
331 def get_json(self, *args, **kwargs):
332 res = self.get(*args, **kwargs)
333
334 # Decode JSON.
335 if res:
336 return json.loads(res)
337
338 ### Misc. actions
339
340 def noop(self):
341 """
342 No operation. Just to check if the connection is
343 working. Returns a random number.
344 """
345 return self.get("/noop")
346
347 def test_code(self, error_code):
348 assert error_code >= 100 and error_code <= 999
349
350 self.get("/error/test/%s" % error_code)
351
352 # Build actions
353
354 def build_create(self, filename, build_type, arches=None, distro=None):
355 """
356 Create a new build on the hub.
357 """
358 assert build_type in ("scratch", "release")
359
360 # XXX Check for permission to actually create a build.
361
362 # Upload the source file to the server.
363 upload_id = self.upload_file(filename)
364
365 data = {
366 "arches" : ",".join(arches or []),
367 "build_type" : build_type,
368 "distro" : distro or "",
369 "upload_id" : upload_id,
370 }
371
372 # Then create the build.
373 build_id = self.get("/builds/create", data=data)
374
375 return build_id or None
376
377 def build_get(self, build_uuid):
378 return self.get_json("/builds/%s" % build_uuid)
379
380 # Job actions
381
382 def job_get(self, job_uuid):
383 return self.get_json("/jobs/%s" % job_uuid)
384
385 # Package actions
386
387 def package_get(self, package_uuid):
388 return self.get_json("/packages/%s" % package_uuid)