]> git.ipfire.org Git - pakfire.git/blame - src/pakfire/hub.py
configrue: Require Python 3.6 or later
[pakfire.git] / src / pakfire / hub.py
CommitLineData
96a8f2a4
MT
1#!/usr/bin/python3
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
22import base64
23import hashlib
24import logging
25import math
26import os.path
27import time
28
29from . import http
30
31from .constants import *
32
33log = logging.getLogger("pakfire.hub")
34log.propagate = 1
35
36class Hub(object):
37 def __init__(self, huburl, username, password):
38 self.username = username
39 self.password = password
96a8f2a4
MT
40
41 # Initialise the HTTP client
42 self.http = http.Client(baseurl=huburl)
43
44 def _request(self, *args, **kwargs):
45 """
46 Wrapper function around the HTTP Client request()
47 function that adds authentication, etc.
48 """
49 kwargs.update({
50 "auth" : (self.username, self.password),
51 })
52
53 return self.http.request(*args, **kwargs)
54
55 # Test functions
56
57 def test(self):
58 """
59 Tests the connection
60 """
7773b2ce 61 return self._request("/noop", decode="ascii")
96a8f2a4
MT
62
63 def test_error(self, code):
64 assert code >= 100 and code <= 999
65
66 self._request("/error/test/%s" % code)
67
68 # Build actions
69
70 def get_build(self, uuid):
71 return self._request("/builds/%s" % uuid, decode="json")
72
73 def create_build(self, path, type, arches=None, distro=None):
74 """
75 Create a new build on the hub
76 """
77 assert type in ("scratch", "release")
78
79 # XXX Check for permission to actually create a build
80
81 # Upload the souce file to the server
82 upload_id = self.upload_file(path)
83
84 data = {
85 "arches" : ",".join(arches or []),
86 "build_type" : type,
87 "distro" : distro or "",
88 "upload_id" : upload_id,
89 }
90
7773b2ce 91 return self._request("/builds/create", data=data, decode="ascii")
96a8f2a4
MT
92
93 # Job actions
94
95 def get_job(self, uuid):
96 return self._request("/jobs/%s" % uuid, decode="json")
97
98 # Package actions
99
100 def get_package(self, uuid):
101 return self._request("/packages/%s" % uuid, decode="json")
102
103 # File uploads
104
105 def upload_file(self, path):
106 uploader = FileUploader(self, path)
107
108 return uploader.upload()
109
110
111class FileUploader(object):
112 """
113 Handles file uploads to the Pakfire Hub
114 """
115 def __init__(self, hub, path):
116 self.hub = hub
117 self.path = path
118
119 @property
120 def filename(self):
121 """
122 Returns the basename of the uploaded file
123 """
124 return os.path.basename(self.path)
125
126 @property
127 def filesize(self):
128 """
129 The filesize of the uploaded file
130 """
131 return os.path.getsize(self.path)
132
133 @staticmethod
134 def _make_checksum(algo, path):
135 h = hashlib.new(algo)
136
137 with open(path, "rb") as f:
138 while True:
139 buf = f.read(CHUNK_SIZE)
140 if not buf:
141 break
142
143 h.update(buf)
144
145 return h.hexdigest()
146
147 def _get_upload_id(self):
148 """
149 Sends some basic information to the hub
150 and requests an upload id.
151 """
152 data = {
153 "filename" : self.filename,
154 "filesize" : self.filesize,
155 "hash" : self._make_checksum("sha1", self.path),
156 }
157
7773b2ce 158 return self.hub._request("/uploads/create", method="GET", decode="ascii", data=data)
96a8f2a4
MT
159
160 def _send_chunk(self, upload_id, chunk):
161 """
162 Sends a chunk at a time
163 """
164 # Compute the SHA512 checksum of this chunk
165 h = hashlib.new("sha512")
166 h.update(chunk)
167
168 # Encode data in base64
169 data = base64.b64encode(chunk)
170
171 # Send chunk to the server
172 self.hub._request("/uploads/%s/sendchunk" % upload_id, method="POST",
173 data={ "chksum" : h.hexdigest(), "data" : data })
174
175 return len(chunk)
176
177 def upload(self):
178 """
179 Main function which runs the upload
180 """
181 # Borrow progressbar from downloader
182 p = self.hub.http._make_progressbar(message=self.filename, value_max=self.filesize)
183
184 with p:
185 # Request an upload ID
186 upload_id = self._get_upload_id()
187 assert upload_id
188
189 log.debug("Starting upload with id %s" % upload_id)
190
191 # Initial chunk size
192 chunk_size = CHUNK_SIZE
193
194 try:
195 with open(self.path, "rb") as f:
196 while True:
197 chunk = f.read(chunk_size)
198 if not chunk:
199 break
200
201 # Save the time when we started to send this bit
202 time_started = time.time()
203
204 # Send the chunk to the server
205 self._send_chunk(upload_id, chunk)
206
207 # Determine the size of the next chunk
208 duration = time_started - time.time()
209 chunk_size = math.ceil(chunk_size / duration)
210
211 # Never let chunk_size drop under CHUNK_SIZE
212 if chunk_size < CHUNK_SIZE:
213 chunk_size = CHUNK_SIZE
214
215 # Update progressbar
914d9ab0 216 p.increment(len(chunk))
96a8f2a4
MT
217
218 # Catch any unhandled exception here, tell the hub to delete the
219 # file and raise the original exception
220 except Exception as e:
221 self.hub._request("/uploads/%s/destroy" % upload_id)
222
223 raise
224
225 # If all went well, we finish the upload
226 else:
227 self.hub._request("/uploads/%s/finished" % upload_id)
228
229 # Return the upload ID if the upload was successful
230 return upload_id